<?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[ JavaScript - 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[ JavaScript - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Wed, 05 Aug 2026 22:41:26 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/javascript/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ A Guide to Modern React Form Architecture: TanStack Form + Zod + Shadcn ]]>
                </title>
                <description>
                    <![CDATA[ Building production-grade forms in React can be a painful experience. It's one of the parts of front-end engineering that most developers are uncomfortable with. Usually, you'll start with a simple co ]]>
                </description>
                <link>https://www.freecodecamp.org/news/a-guide-to-modern-react-form-architecture-tanstack-form-zod-shadcn/</link>
                <guid isPermaLink="false">6a68d8cf34380fc31276ad8a</guid>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                    <category>
                        <![CDATA[ forms ]]>
                    </category>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Tapas Adhikary ]]>
                </dc:creator>
                <pubDate>Tue, 28 Jul 2026 16:29:03 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/9383f6c8-b938-4540-83cc-42745cdbc943.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Building production-grade forms in React can be a painful experience. It's one of the parts of front-end engineering that most developers are uncomfortable with.</p>
<p>Usually, you'll start with a simple controlled form using the <code>useState</code> hook. But as the form grows, you realise that typing a single character into an input field triggers a re-render of the entire component tree. The application becomes sluggish and ends up creating a terrible user experience.</p>
<p>You might even try mitigating this re-rendering issue by switching to an uncontrolled form using the <code>useRef</code> hook. But this introduces horrible scalability issues, and as the form grows, maintaining the code becomes a nightmare.</p>
<p>Vibe coding might get you to a working demo, but actual engineering gets you to production. To fix these performance and scalability issues in the production apps, we need to rethink how inputs work.</p>
<p>In this article, we'll build a production-ready form architecture. We'll use <code>TanStack Form</code> as a headless state machine to solve the performance and scalability problems, <code>Zod</code> for bulletproof validations, and <code>ShadCN UI</code> for accessible, beautiful components.</p>
<p>If you're a visual learner, I've recorded a full masterclass video covering this exact architecture over on my YouTube channel, <a href="https://www.youtube.com/tapasadhikary">tapaScript</a>. You can watch it right here:</p>
<div class="embed-wrapper"><iframe width="560" height="315" src="https://www.youtube.com/embed/qSR6UeSKnT0" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>

<h2 id="heading-table-of-contents">Table of Contents</h2>
<ol>
<li><p><a href="#heading-the-problem-with-traditional-forms">The Problem with Traditional Forms</a></p>
<ul>
<li><p><a href="#heading-the-uncontrolled-ref-workaround">The Uncontrolled "Ref" Workaround</a></p>
</li>
<li><p><a href="#heading-why-not-just-use-native-html-forms">Why Not Just Use Native HTML Forms?</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-introducing-tanstack-form">Introducing TanStack Form</a></p>
</li>
<li><p><a href="#heading-fine-grained-reactivity">Fine-Grained Reactivity</a></p>
</li>
<li><p><a href="#heading-validation-with-zod">Validation with Zod</a></p>
</li>
<li><p><a href="#heading-headless-tanstack-form-with-shadcn-ui">Headless TanStack Form with ShadCN UI</a></p>
</li>
<li><p><a href="#heading-handling-nested-dynamic-arrays-with-tanstack-form">Handling Nested Dynamic Arrays with TanStack Form</a></p>
</li>
<li><p><a href="#heading-tanstack-forms-granular-reactivity-useselector-vs-formsubscribe">TanStack Form's Granular Reactivity: <code>useSelector</code> vs <code>form.Subscribe</code></a></p>
<ul>
<li><p><a href="#heading-with-useselector">With <code>useSelector()</code></a></p>
</li>
<li><p><a href="#heading-with-formsubscribe">With <code>form.Subscribe()</code></a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-tanstack-form-vs-react-hook-form">TanStack Form vs. React Hook Form</a></p>
<ul>
<li><p><a href="#heading-dynamic-arrays">Dynamic Arrays</a></p>
</li>
<li><p><a href="#heading-deeply-nested-components">Deeply Nested Components</a></p>
</li>
<li><p><a href="#id=%22heading-the-ecosystem-sync%22">The Ecosystem Sync</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-conclusion-amp-important-resources">Conclusion &amp; Important Resources</a></p>
</li>
<li><p><a href="#heading-if-youve-read-this-far">If You've Read This Far...</a></p>
</li>
</ol>
<h2 id="heading-the-problem-with-traditional-forms">The Problem with Traditional Forms</h2>
<p>When developers realize that <code>useState</code> gives them the control to manage forms using React's state, they often create form components like this:</p>
<pre><code class="language-typescript">import { useState } from "react";

export function StandardRegistrationForm() {
 
  const [formData, setFormData] = useState({
    firstName: "",
    lastName: "",
    email: "",
    password: "",
  });

  console.log("Form Re-rendered! Current state:", formData.firstName);

  const handleChange = (e: React.ChangeEvent&lt;HTMLInputElement&gt;) =&gt; {
    setFormData((prev) =&gt; ({
      ...prev,
      [e.target.name]: e.target.value,
    }));
  };

  return (
    &lt;div className="bg-neutral-900 border border-neutral-800 rounded-xl p-6 w-full max-w-md mx-auto mt-10 text-white"&gt;
      &lt;div className="mb-6"&gt;
        &lt;h2 className="text-xl font-bold text-red-500"&gt;The Problem&lt;/h2&gt;
        &lt;p className="text-sm text-neutral-400"&gt;Standard Controlled Form&lt;/p&gt;
      &lt;/div&gt;

      &lt;form className="space-y-4"&gt;
        &lt;div className="flex flex-col gap-2"&gt;
          &lt;label className="text-sm font-medium text-neutral-300"&gt;
            First Name
          &lt;/label&gt;
          &lt;input
            name="firstName"
            value={formData.firstName}
            onChange={handleChange}
            className="p-2 rounded-md bg-neutral-950 border border-neutral-700 focus:border-red-500 focus:outline-none"
          /&gt;
        &lt;/div&gt;

        &lt;div className="flex flex-col gap-2"&gt;
          &lt;label className="text-sm font-medium text-neutral-300"&gt;
            Last Name
          &lt;/label&gt;
          &lt;input
            name="lastName"
            value={formData.lastName}
            onChange={handleChange}
            className="p-2 rounded-md bg-neutral-950 border border-neutral-700 focus:border-red-500 focus:outline-none"
          /&gt;
        &lt;/div&gt;

        &lt;div className="flex flex-col gap-2"&gt;
          &lt;label className="text-sm font-medium text-neutral-300"&gt;
            Email
          &lt;/label&gt;
          &lt;input
            name="email"
            type="email"
            value={formData.email}
            onChange={handleChange}
            className="p-2 rounded-md bg-neutral-950 border border-neutral-700 focus:border-red-500 focus:outline-none"
          /&gt;
        &lt;/div&gt;

        &lt;button
          type="button"
          className="w-full mt-4 bg-neutral-800 text-neutral-400 py-2 rounded-md font-bold cursor-not-allowed"
        &gt;
          Sign Up
        &lt;/button&gt;
      &lt;/form&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p>The component uses a centralized <code>useState</code> object, which is the standard way most developers are taught to build forms. The moment a value changes for any input fields, the state gets updated.</p>
<p>In React, every time you update a state, the component re-renders to display the current state value. This introduces a performance issue that you need to solve.</p>
<p>The image below shows the exact performance problem of the form component getting re-rendered with every keystroke inside the input elements.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5c9bb4026656f09759cdc1f0/e80e63f0-96c3-4a7d-bd39-6c13ccc71282.gif" alt="The component re-rendering" style="display:block;margin:0 auto" width="1228" height="1158" loading="lazy">

<h3 id="heading-the-uncontrolled-ref-workaround">The Uncontrolled "Ref" Workaround</h3>
<p>Acknowledging the problem we've seen with the controlled form component, devs found a workaround. Instead of using state, if we use ref, the components don't re-render on ref's current value changes. That sounds like an AHA! moment until we figure out that it introduces completely different architectural headaches.</p>
<p>Here's the same form, but managed using refs instead of states:</p>
<pre><code class="language-typescript">import { useRef } from "react";

export function RefRegistrationForm() {
 
  const firstNameRef = useRef&lt;HTMLInputElement&gt;(null);
  const lastNameRef = useRef&lt;HTMLInputElement&gt;(null);
  const emailRef = useRef&lt;HTMLInputElement&gt;(null);

  console.log("Form Rendered (Notice it doesn't log when typing)");

  const handleSubmit = (e: React.FormEvent) =&gt; {
    e.preventDefault();

    const formData = {
      firstName: firstNameRef.current?.value,
      lastName: lastNameRef.current?.value,
      email: emailRef.current?.value,
    };

    console.log("Submitted Data:", formData);

  };

  return (
    &lt;div className="bg-neutral-900 border border-neutral-800 rounded-xl p-6 w-full max-w-md mx-auto mt-10 text-white"&gt;
      &lt;div className="mb-6"&gt;
        &lt;h2 className="text-xl font-bold text-orange-500"&gt;The Ref Workaround&lt;/h2&gt;
        &lt;p className="text-sm text-neutral-400"&gt;Fast, but scales horribly.&lt;/p&gt;
      &lt;/div&gt;

      &lt;form onSubmit={handleSubmit} className="space-y-4"&gt;
        &lt;div className="flex flex-col gap-2"&gt;
          &lt;label className="text-sm font-medium text-neutral-300"&gt;
            First Name
          &lt;/label&gt;
          &lt;input
            name="firstName"
            ref={firstNameRef} // Attaching the ref
            className="p-2 rounded-md bg-neutral-950 border border-neutral-700 focus:border-orange-500 focus:outline-none"
          /&gt;
        &lt;/div&gt;

        &lt;div className="flex flex-col gap-2"&gt;
          &lt;label className="text-sm font-medium text-neutral-300"&gt;
            Last Name
          &lt;/label&gt;
          &lt;input
            name="lastName"
            ref={lastNameRef} // Attaching the ref
            className="p-2 rounded-md bg-neutral-950 border border-neutral-700 focus:border-orange-500 focus:outline-none"
          /&gt;
        &lt;/div&gt;

        &lt;div className="flex flex-col gap-2"&gt;
          &lt;label className="text-sm font-medium text-neutral-300"&gt;
            Email
          &lt;/label&gt;
          &lt;input
            name="email"
            type="email"
            ref={emailRef} // Attaching the ref
            className="p-2 rounded-md bg-neutral-950 border border-neutral-700 focus:border-orange-500 focus:outline-none"
          /&gt;
        &lt;/div&gt;

        &lt;button
          type="submit"
          className="w-full mt-4 bg-orange-600 hover:bg-orange-500 py-2 rounded-md font-bold transition-colors"
        &gt;
          Sign Up
        &lt;/button&gt;
      &lt;/form&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p>Let's talk about the problems with this approach:</p>
<ul>
<li><p>The Ref Soup: We've created a separate ref for every single input. Imagine this form grows to 20+ fields, and you have to manage all those refs!</p>
</li>
<li><p>We need to extract the data manually and stitch the object back together. There's no mechanism like <code>setState()</code> that exists here.</p>
</li>
<li><p>How do we do real-time validation? Since the field/component doesn't re-render here as you type, showing a dynamic error message under the inputs becomes incredibly hard without introducing new state variables, and doing that would defeat the whole purpose of refs.</p>
</li>
</ul>
<h3 id="heading-why-not-just-use-native-html-forms">Why Not Just Use Native HTML Forms?</h3>
<p>Oh yes! We shouldn't ignore this possibility as well. Why use a library at all? Can't we just use native HTML forms and grab the data with FormData on submit?</p>
<p>If you're building a simple "Contact Us" or "Login Form", the native HTML forms are actually the best tool for the job. They're lean, require zero library knowledge, and are incredibly efficient.</p>
<p>But for complex, production-grade web applications, the native approach quickly hits a wall:</p>
<ol>
<li><p>Complex data structures: <code>FormData</code> is a flat object. If you're dealing with deeply nested objects or dynamic arrays, manually parsing <code>speakers[0][firstName]</code> out of a flat FormData object could be messy.</p>
</li>
<li><p>Dynamic UI: If you have a requirement where Field B should appear if Field A is set to "Yes", you need client-side state to track Field A's value anyway. FormData only exists at the exact moment of submission.</p>
</li>
<li><p>UX &amp; Real-time Validations: Native HTML validations are rigid and hard to style cleanly across browsers. If you need complex cross-field validations (like confirming passwords match) or async validations (checking if a username is taken while the user types), you need JavaScript.</p>
</li>
</ol>
<p>When native forms aren't enough, we move to React.</p>
<h2 id="heading-introducing-tanstack-form">Introducing TanStack Form</h2>
<p><a href="https://tanstack.com/form/latest">TanStack Form</a> is a headless state machine for your inputs. It gives you the strict typing and easy data management of a controlled form, but uses fine-grained reactivity to ensure that only the specific field you're typing in re-renders, and nothing else.</p>
<p>Let's set it up in a modern Vite + TypeScript environment. First, install the TanStack Form using this command:</p>
<pre><code class="language-shell">npm install @tanstack/react-form # Or use, equivallent yarn, pnpm commands
</code></pre>
<p>Instead of a massive list of individual state hooks, you just need the <code>useForm</code> hook to spin up the form engine:</p>
<pre><code class="language-typescript">import { useForm } from '@tanstack/react-form';

export function SpeakerForm() {
  const form = useForm({
    defaultValues: {
      firstName: '',
      lastName: '',
      twitterHandle: '',
    },
    onSubmit: async ({ value }) =&gt; {
      console.log('Form Submitted!', value);
    },
  });

  return (
    &lt;div className="bg-neutral-900 border border-neutral-800 rounded-xl p-6 max-w-md mx-auto text-white"&gt;
      &lt;h2 className="text-xl font-bold mb-6"&gt;Add New Speaker&lt;/h2&gt;
      {/* We will build the fields here */}
    &lt;/div&gt;
  );
}
</code></pre>
<p>Notice the <code>defaultValues</code>. TanStack Form automatically infers your entire form structure from this object. If you try to reference an invalid field name later, TypeScript will throw an error immediately.</p>
<h2 id="heading-fine-grained-reactivity">Fine-Grained Reactivity</h2>
<p>Let's now connect the form inputs to the engine. To do that, we need to wrap them in TanStack's <code>Field</code> component. This uses the standard JSX <a href="https://www.youtube.com/watch?v=tIdJj0n1mg4">render prop pattern</a> to pass the isolated field state directly into our UI.</p>
<pre><code class="language-typescript">&lt;form
  onSubmit={(e) =&gt; {
    e.preventDefault();
    e.stopPropagation();
    form.handleSubmit();
  }}
  className="space-y-4"
&gt;
  &lt;form.Field name="firstName"&gt;
    {(field) =&gt; {
      // PROOF: This log will only fire when firstName changes!
      console.log("Rendering First Name Field");
      
      return (
        &lt;div className="flex flex-col gap-2"&gt;
          &lt;label htmlFor={field.name} className="text-sm text-neutral-300"&gt;
            First Name
          &lt;/label&gt;
          &lt;input
            id={field.name}
            name={field.name}
            value={field.state.value}
            onBlur={field.handleBlur}
            onChange={(e) =&gt; field.handleChange(e.target.value)}
            className="p-2 rounded-md bg-neutral-950 border border-neutral-700 focus:border-emerald-500 outline-none"
          /&gt;
        &lt;/div&gt;
      );
    }}
  &lt;/form.Field&gt;
  
  &lt;button type="submit" className="w-full mt-4 bg-emerald-600 py-2 rounded-md font-bold"&gt;
    Save Speaker
  &lt;/button&gt;
&lt;/form&gt;
</code></pre>
<p>If you place a <code>console.log</code> in the parent component and one inside the field, you'll see the magic. When you type in the First Name input, the parent component doesn't re-render. Here's a visual of only the form field re-rendering.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5c9bb4026656f09759cdc1f0/0ffbaa73-0d42-41d7-bbb3-eeb12e51ad62.gif" alt="TanStack Form" style="display:block;margin:0 auto" width="1134" height="628" loading="lazy">

<p>The state update is strictly isolated to the function inside that specific <code>Field</code> component.</p>
<h2 id="heading-validation-with-zod">Validation with Zod</h2>
<p>A form is only useful if it has field validations. In the current ecosystem, <a href="https://zod.dev/">Zod</a> is the industry standard. First, install Zod and the zod form adapter.</p>
<pre><code class="language-shell">npm install zod @tanstack/zod-form-adapter # Or use, equivallent yarn, pnpm commands
</code></pre>
<p>Then inject the Zod adapter into your form engine:</p>
<pre><code class="language-typescript">import { zodValidator } from '@tanstack/zod-form-adapter';
import { z } from 'zod';

// Inside your component...
const form = useForm({
  defaultValues: { firstName: '', lastName: '' },
  validatorAdapter: zodValidator(),
  onSubmit: async ({ value }) =&gt; { /* ... */ },
});
</code></pre>
<p>Next, we'll update our field to use Zod rules. Here we opted for a rule that the first name must be at least 2 characters. Also, by checking the <code>field.state.meta.isTouched</code>, we ensure the error only shows after the user interacts with the input.</p>
<pre><code class="language-typescript">
&lt;form.Field
  name="firstName"
  validators={{
    onChange: z.string().min(2, "First name must be at least 2 characters"),
  }}
&gt;
  {(field) =&gt; (
    &lt;div className="flex flex-col gap-2"&gt;
      &lt;label htmlFor={field.name} className="text-neutral-300"&gt;
        First Name
      &lt;/label&gt;
      
      &lt;input
        id={field.name}
        name={field.name}
        value={field.state.value}
        onBlur={field.handleBlur}
        onChange={(e) =&gt; field.handleChange(e.target.value)}
        className="bg-neutral-950 border-neutral-700 focus-visible:ring-emerald-500"
      /&gt;
      {field.state.meta.isTouched &amp;&amp; field.state.meta.errors.length ? (
        &lt;em className="text-red-500 text-xs"&gt;
        {
           field.state.meta.errors.map((error) =&gt;  error.message)
         }
        &lt;/em&gt;) : null}
    
    &lt;/div&gt;
  )}
&lt;/form.Field&gt;
</code></pre>
<p>The image below shows how the error message appears below the field when the First Name field value is changed and it fails validation.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5c9bb4026656f09759cdc1f0/13ec370d-68d9-450e-badc-5a0269816231.png" alt="Form Validation" style="display:block;margin:0 auto" width="1135" height="678" loading="lazy">

<h2 id="heading-headless-tanstack-form-with-shadcn-ui">Headless TanStack Form with ShadCN UI</h2>
<p>TanStack Form is headless. This means you can map it to any design system seamlessly. Let's take one of the modern design systems available today: <a href="https://ui.shadcn.com/">ShadCN UI</a>.</p>
<p>If you are using a Vite-TypeScript-based React project, you can integrate the ShadCN UI in a few simple steps:</p>
<p>First, open the <code>tsconfig.json</code> file and add this compiler option:</p>
<pre><code class="language-json">"compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": [
        "./src/*"
      ]
    }
  }
</code></pre>
<p>Then open the <code>vite.config.json</code> file and add the mapping for the <code>@</code> alias:</p>
<pre><code class="language-typescript">import react from '@vitejs/plugin-react'
import path from "path"
import { defineConfig } from 'vite'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
    },
  },
})
</code></pre>
<p>Install shadcn and its components like label, input, and button:</p>
<pre><code class="language-shell">npx shadcn@latest add input label button
</code></pre>
<p>Now, you can import these ShadCN components into your form component and replace the native HTML label, input, and button.</p>
<pre><code class="language-typescript">import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";

&lt;form.Field
  name="firstName"
  validators={{
    onChange: z.string().min(2, "First name must be at least 2 characters"),
  }}
&gt;
  {(field) =&gt; (
    &lt;div className="flex flex-col gap-2"&gt;
      &lt;Label htmlFor={field.name} className="text-neutral-300"&gt;
        First Name
      &lt;/Label&gt;
      
      &lt;Input
        id={field.name}
        name={field.name}
        value={field.state.value}
        onBlur={field.handleBlur}
        onChange={(e) =&gt; field.handleChange(e.target.value)}
        className="bg-neutral-950 border-neutral-700 focus-visible:ring-emerald-500"
      /&gt;
      
       {field.state.meta.isTouched &amp;&amp; field.state.meta.errors.length ? (
        &lt;em className="text-red-500 text-xs"&gt;
        {
           field.state.meta.errors.map((error) =&gt;  error.message)
         }
        &lt;/em&gt;) : null}
    
    &lt;/div&gt;
  )}
&lt;/form.Field&gt;
</code></pre>
<h2 id="heading-handling-nested-dynamic-arrays-with-tanstack-form">Handling Nested Dynamic Arrays with TanStack Form</h2>
<p>Suppose we're building a speaker form for a meetup. What if we want to feature multiple speakers for a talk in our meetup? Now we need to manage a data structure to hold more than one speaker's details. We'll need an array of objects.</p>
<p>In classic React, managing an array of objects with complex states leads to spaghetti code. TanStack Form treats arrays exactly like a typical form field. We just need to update our <code>defaultValues</code> to an array:</p>
<pre><code class="language-typescript">defaultValues: {
  speakers: [ { firstName: '', lastName: '' } ],
},
</code></pre>
<p>Now, set the mode to <code>array</code> on your field and map over it:</p>
<pre><code class="language-typescript">&lt;form.Field name="speakers" mode="array"&gt;
  {(field) =&gt; (
    &lt;div className="space-y-6"&gt;
      {field.state.value.map((_, index) =&gt; (
        &lt;div key={index} className="p-4 border border-neutral-800 rounded-lg relative"&gt;
          
          &lt;div className="grid grid-cols-2 gap-4"&gt;
            {/* Nested Fields use bracket notation! */}
            &lt;form.Field name={`speakers[${index}].firstName`}&gt;
              {(subField) =&gt; (
                 &lt;div&gt;
                    &lt;Label&gt;First Name&lt;/Label&gt;
                    &lt;Input 
                      value={subField.state.value} 
                      onChange={(e) =&gt; subField.handleChange(e.target.value)} 
                    /&gt;
                 &lt;/div&gt;
              )}
            &lt;/form.Field&gt;
          &lt;/div&gt;

          {/* Remove Button */}
          {field.state.value.length &gt; 1 &amp;&amp; (
            &lt;Button 
              type="button" 
              variant="destructive" 
              onClick={() =&gt; field.removeValue(index)}
            &gt;
              Remove
            &lt;/Button&gt;
          )}
        &lt;/div&gt;
      ))}
      
      {/* Add Button */}
      &lt;Button
        type="button"
        variant="outline"
        onClick={() =&gt; field.pushValue({ firstName: '', lastName: '' })}
      &gt;
        + Add Speaker
      &lt;/Button&gt;
    &lt;/div&gt;
  )}
&lt;/form.Field&gt;
</code></pre>
<p>To append data, we call <code>field.pushValue()</code> and to remove, we call <code>field.removeValue(index)</code>. Typing into the newest row leaves the previous array fields completely static. It's incredibly performant.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5c9bb4026656f09759cdc1f0/2a5552f6-2103-441f-ac47-758796985eae.png" alt="Array of Object" style="display:block;margin:0 auto" width="2315" height="1068" loading="lazy">

<p>The image above shows the multi-speaker form and its output when submitted.</p>
<h2 id="heading-tanstack-forms-granular-reactivity-useselector-vs-formsubscribe">TanStack Form's Granular Reactivity: <code>useSelector</code> vs <code>form.Subscribe</code></h2>
<p>Sometimes, you need to access form state outside an input. For example, you want to show the "Unsaved Changes!" notification the moment the user changes the value in the input fields.</p>
<p>Notice that here the notification is outside the form, but it solely depends on the form field values.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5c9bb4026656f09759cdc1f0/e22c66a8-7ebc-40df-b598-fe91eb257182.png" alt="unsaved data" style="display:block;margin:0 auto" width="903" height="647" loading="lazy">

<p>TanStack gives you two distinct tools for this, and knowing when to use them is the key to mastering this architecture.</p>
<h3 id="heading-with-useselector">With <code>useSelector()</code></h3>
<p>If the state change affects the macro-layout of your page (like showing a global "unsaved Changes" banner like the above), you want the parent component to re-render. We'll use the standalone <code>useSelector()</code> hook for this:</p>
<pre><code class="language-typescript">import { useForm, useSelector } from "@tanstack/react-form";

export function ReactivityDemo() {
  const form = useForm({ defaultValues: { bio: "" } });

  // This isolates the exact piece of state we want the parent to track
  const isFormDirty = useSelector(form.store, (state) =&gt; state.isDirty);

  return (
    &lt;div className="relative"&gt;
      {isFormDirty &amp;&amp; (
        &lt;div className="absolute top-0 w-full bg-orange-500 text-center"&gt;
          Unsaved Changes!
        &lt;/div&gt;
      )}
      {/* Form goes here */}
    &lt;/div&gt;
  );
}
</code></pre>
<h3 id="heading-with-formsubscribe">With <code>form.Subscribe()</code></h3>
<p>If the state change only affects a micro-interaction like disabling a submit button while the form is being saved, we don't want the whole form to re-render. In that case, we wrap the button tightly inside <code>&lt;form.Subscribe&gt;</code>.</p>
<pre><code class="language-typescript">&lt;form.Subscribe selector={(state) =&gt; [state.canSubmit, state.isSubmitting]}&gt;
  {([canSubmit, isSubmitting]) =&gt; (
    &lt;Button
      type="submit"
      disabled={!canSubmit || isSubmitting}
      className="w-full mt-4"
    &gt;
      {isSubmitting ? "Saving..." : "Save Changes"}
    &lt;/Button&gt;
  )}
&lt;/form.Subscribe&gt;
</code></pre>
<p>This isolates the re-render exclusively to the button. The rest of the component stays completely undisturbed.</p>
<h2 id="heading-tanstack-form-vs-react-hook-form">TanStack Form vs. React Hook Form</h2>
<p>If you've been building React applications for a while, you're probably thinking: "Why not just use React Hook Form?"</p>
<p><code>React Hook Form</code> is an extraordinary library and has been the industry standard for years. It solves the performance problem by leveraging uncontrolled components and <code>useRef</code> under the hood. It isolates re-renders perfectly and handles validation well.</p>
<p>But as applications scale in complexity, the TanStack Form architecture offers three distinct advantages over React Hook Form:</p>
<h3 id="heading-dynamic-arrays">Dynamic Arrays</h3>
<p>If you've ever built a complex form with React Hook Form that needs dynamic, nested arrays (like adding multiple speakers in our example above), you probably need a hook like <code>useFieldArray</code>. Managing complex useFieldArray implementations often needs huge boilerplate, careful index tracking, and jumping through to maintain strict TypeScript safety deep within the tree.</p>
<p>TanStack Form eliminates this. It treats arrays the same as a string input. The form is a centralized state machine: you simply inform a field that it has a <code>mode=array</code>, and you instantly get access to <code>pushValue()</code> and <code>removeValue()</code> along with deeply nested type-safety.</p>
<h3 id="heading-deeply-nested-components">Deeply Nested Components</h3>
<p>React Hook Form relies on uncontrolled inputs to achieve performance. While this is fast, passing refs around deeply nested component trees can get messy, especially when integrating with complex UI libraries.</p>
<p>TanStack Form gives us the strict, predictable architecture of a controlled component but uses fine-grained reactivity to ensure only the specific parts of the UI that change are re-rendered.</p>
<h3 id="heading-the-ecosystem-sync">The Ecosystem Sync</h3>
<p>If you're moving towards modern, production-ready stacks using TanStack Query (React Query) for data fetching and TanStack Router for navigation, TanStack Form shares the exact same mental model and family. It integrates seamlessly into the ecosystem, providing a unified DX across your entire application architecture.</p>
<h2 id="heading-conclusion-amp-important-resources">Conclusion &amp; Important Resources</h2>
<p>By combining TanStack Form, Zod, and ShadCN UI, we've created a strictly typed, reactive, production-ready form architecture that handles everything from basic text inputs to complex nested arrays without sacrificing any bit of performance.</p>
<p>You can grab the complete starter code and the final project from my GitHub:</p>
<ul>
<li><p>All the source code used in the article and the complete project source code: <a href="https://github.com/tapascript/full-stack-vibe-to-prod/tree/main/11-tanstack-form">https://github.com/tapascript/full-stack-vibe-to-prod/tree/main/11-tanstack-form</a></p>
</li>
<li><p>A code scaffolding repo for React projects using TypeScript, Vite, and TailwindCSS: <a href="https://github.com/atapas/code-react19-ts">https://github.com/atapas/code-react19-ts</a></p>
</li>
</ul>
<p>If you found this helpful, you'll find these two in-depth video tutorials helpful, too:</p>
<ul>
<li><p><a href="https://www.youtube.com/watch?v=9VnPRZ0F7yc">TanStack Router Crash Course</a></p>
</li>
<li><p><a href="https://www.youtube.com/watch?v=Hu1dtgK_CkU">TanStack Query With Projects</a></p>
</li>
</ul>
<h2 id="heading-if-youve-read-this-far"><strong>If You've Read This Far...</strong></h2>
<p>Thank You!</p>
<p>I'm thrilled to announce that I've started a <a href="https://www.youtube.com/playlist?list=PLIJrr73KDmRwySan3tObLmLZp0NYWSmCT">Full Stack FREE Course</a> to take developers from vibe coding to a production-ready mental model. I'd be delighted if you check it out and take part.</p>
<ul>
<li><p>Subscribe to my <a href="https://www.youtube.com/tapasadhikary?sub_confirmation=1">YouTube Channel</a></p>
</li>
<li><p>Follow on <a href="https://www.linkedin.com/in/tapasadhikary/">LinkedIn</a> and <a href="https://x.com/tapasadhikary">X</a></p>
</li>
<li><p>Catch up with my <a href="https://www.tapascript.io/books/react-clean-code-rule-book">React Clean Code Rules Book</a></p>
</li>
<li><p>All the source code used in this article is on my <a href="https://github.com/tapascript/full-stack-vibe-to-prod">GitHub Repository</a>.</p>
</li>
</ul>
<p>See you soon with my next article. Until then, please take care of yourself and keep learning.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Browser-Based PDF Blur Tool Using JavaScript ]]>
                </title>
                <description>
                    <![CDATA[ Many PDF documents contain information that shouldn't be shared publicly. Personal details, financial figures, signatures, addresses, account numbers, employee information, or confidential business da ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-pdf-blur-tool-javascript/</link>
                <guid isPermaLink="false">6a63cec8c741f882378e9b06</guid>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ pdf ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Online PDF Tools ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Tutorial ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Bhavin Sheth ]]>
                </dc:creator>
                <pubDate>Fri, 24 Jul 2026 20:44:56 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/ca664176-3589-4e16-b95e-506fa53bcfce.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Many PDF documents contain information that shouldn't be shared publicly. Personal details, financial figures, signatures, addresses, account numbers, employee information, or confidential business data often need to be hidden before a file is sent to someone else.</p>
<p>A PDF Blur Tool makes this process simple. Instead of permanently removing content, it places a blur effect over selected areas of a PDF so sensitive information becomes difficult to read while the rest of the document remains unchanged.</p>
<p>In this tutorial, you'll build a browser-based PDF Blur Tool using JavaScript. Users will be able to upload a PDF, preview every page, draw blur boxes over sensitive content, adjust the blur intensity, apply the blur to selected pages, preview the final result, and download the processed PDF, all without uploading files to a server.</p>
<p>We'll use PDF.js to render PDF pages inside the browser, HTML Canvas to create and manage blur regions, and PDF-lib to generate the final blurred PDF.</p>
<p>By the end of this tutorial, you'll have a fully functional client-side PDF editing tool similar to the one available on my site, AllInOneTools.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/76925a1f-2b94-4b5b-923c-0252d1703b22.png" alt="allinonetools - pdf tools- blur pdf documents" style="display:block;margin:0 auto" width="905" height="282" loading="lazy">

<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-what-this-pdf-blur-tool-does-and-how-it-works">What This PDF Blur Tool Does and How It Works</a></p>
</li>
<li><p><a href="#heading-project-setup">Project Setup</a></p>
</li>
<li><p><a href="#heading-creating-the-html-layout">Creating the HTML Layout</a></p>
</li>
<li><p><a href="#heading-uploading-and-previewing-pdfs">Uploading and Previewing PDFs</a></p>
</li>
<li><p><a href="#heading-creating-blur-regions">Creating Blur Regions</a></p>
</li>
<li><p><a href="#heading-applying-blur-to-pages">Applying Blur to Pages</a></p>
</li>
<li><p><a href="#heading-generating-the-final-pdf">Generating the Final PDF</a></p>
</li>
<li><p><a href="#heading-previewing-the-result">Previewing the Result</a></p>
</li>
<li><p><a href="#heading-renaming-and-downloading">Renaming and Downloading</a></p>
</li>
<li><p><a href="#heading-demo-how-the-pdf-blur-tool-works">Demo: How the PDF Blur Tool Works</a></p>
</li>
<li><p><a href="#heading-performance-tips">Performance Tips</a></p>
</li>
<li><p><a href="#heading-common-mistakes">Common Mistakes</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-what-this-pdf-blur-tool-does-and-how-it-works">What This PDF Blur Tool Does and How It Works</h2>
<p>A PDF Blur Tool helps protect sensitive information before a document is shared. Instead of editing or deleting the original content, it applies a visual blur effect over selected areas so confidential information becomes unreadable while the rest of the document remains unchanged.</p>
<p>This approach is useful for hiding personal details, financial information, account numbers, signatures, addresses, faces, or any other private content that shouldn't be visible in the final document.</p>
<p>In this project, users can upload a PDF directly from their browser, preview every page, and draw one or more blur regions over the areas they want to hide. The tool also allows users to adjust the blur intensity, blur either selected areas or entire pages, apply the effect to the current page, all pages, or specific page ranges, preview the completed document, rename the output file, and download the final PDF.</p>
<p>Because everything runs inside the browser, no files are uploaded to a server, helping maintain document privacy.</p>
<p>Behind the scenes, the application first renders each PDF page onto an HTML canvas using PDF.js. Rather than modifying the original PDF immediately, it records the position, size, page number, and blur intensity for every blur region that the user creates.</p>
<p>When the user clicks Apply &amp; Finalize, those stored regions are converted from browser coordinates into actual PDF page coordinates. The selected blur effect is then applied to the rendered page, and PDF-lib generates a new PDF containing the blurred content while preserving the rest of the document.</p>
<p>This workflow provides an interactive editing experience while keeping the original PDF unchanged until the final document is generated.</p>
<p>For example, each blur region can be represented as an object like this:</p>
<pre><code class="language-javascript">const blurRegion = {

    page: 2,

    x: 180,

    y: 240,

    width: 260,

    height: 90,

    intensity: 6

};
</code></pre>
<p>Each object stores all the information required to recreate the blur effect during the final PDF generation process.</p>
<h2 id="heading-project-setup">Project Setup</h2>
<p>Before writing any code, let's create a simple project structure for our PDF Blur Tool.</p>
<p>We'll use plain HTML, CSS, and JavaScript, along with two libraries:</p>
<ul>
<li><p><strong>PDF.js</strong> for rendering PDF pages inside the browser.</p>
</li>
<li><p><strong>PDF-lib</strong> for generating the final blurred PDF.</p>
</li>
</ul>
<p>Our project structure looks like this:</p>
<pre><code class="language-text">pdf-blur-tool/

│── index.html
│── style.css
│── script.js
│── pdf.worker.min.js
│── assets/
</code></pre>
<p>Keeping the project simple makes it easier to understand how each part works.</p>
<p>Add PDF.js and PDF-lib before loading your own JavaScript.</p>
<pre><code class="language-html">&lt;script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.4.168/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 worker.</p>
<pre><code class="language-javascript">pdfjsLib.GlobalWorkerOptions.workerSrc =
    "pdf.worker.min.js";
</code></pre>
<p>The worker processes PDF rendering in a background thread, helping keep the interface responsive while pages are rendered.</p>
<h2 id="heading-creating-the-html-layout">Creating the HTML Layout</h2>
<p>The application consists of four main sections:</p>
<ul>
<li><p>Upload area</p>
</li>
<li><p>PDF preview</p>
</li>
<li><p>Blur settings panel</p>
</li>
<li><p>Final download section</p>
</li>
</ul>
<p>Create the basic layout:</p>
<pre><code class="language-html">&lt;div id="uploadSection"&gt;&lt;/div&gt;

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

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

    &lt;aside id="blurSettings"&gt;&lt;/aside&gt;

&lt;/div&gt;

&lt;div id="resultSection" hidden&gt;&lt;/div&gt;
</code></pre>
<p>Initially, only the upload section is visible.</p>
<p>After a PDF is selected, the editor becomes visible.</p>
<h3 id="heading-selecting-dom-elements">Selecting DOM Elements</h3>
<p>Create references to the elements used throughout the application.</p>
<pre><code class="language-javascript">const uploadSection =
    document.getElementById(
        "uploadSection"
    );

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

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

const fileInput =
    document.getElementById(
        "pdfInput"
    );

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

const canvasContext =
    pdfCanvas.getContext("2d");
</code></pre>
<p>These references allow the application to switch between the upload, editing, and download stages.</p>
<h2 id="heading-uploading-and-previewing-pdfs">Uploading and Previewing PDFs</h2>
<p>The upload section accepts both drag-and-drop and traditional file selection.</p>
<p>When a PDF is chosen, verify that it's actually a PDF before continuing.</p>
<pre><code class="language-javascript">async function handlePdfUpload(
    file
) {

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

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

        return;

    }

    await loadPdf(file);

}
</code></pre>
<p>If validation succeeds, the document is loaded into memory.</p>
<h3 id="heading-reading-the-pdf">Reading the PDF</h3>
<p>Use the File API to convert the uploaded file into an ArrayBuffer.</p>
<pre><code class="language-javascript">async function loadPdf(
    file
) {

    const bytes =
        await file.arrayBuffer();

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

    currentPage = 1;

    await renderPage(
        currentPage
    );

}
</code></pre>
<p>The uploaded bytes will also be reused later when generating the blurred PDF.</p>
<h3 id="heading-rendering-the-first-page">Rendering the First Page</h3>
<p>PDF.js renders each page onto an HTML canvas.</p>
<p>Start by retrieving the requested page.</p>
<pre><code class="language-javascript">async function renderPage(
    pageNumber
) {

    const page =
        await pdfDocument
            .getPage(
                pageNumber
            );

    const viewport =
        page.getViewport({
            scale: 1.5
        });
</code></pre>
<p>Resize the canvas to match the page dimensions.</p>
<pre><code class="language-javascript">pdfCanvas.width =
    viewport.width;

pdfCanvas.height =
    viewport.height;
</code></pre>
<p>Render the page.</p>
<pre><code class="language-javascript">await page.render({

    canvasContext,

    viewport

}).promise;
</code></pre>
<p>After rendering finishes, the PDF page becomes visible inside the editor.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/0dbda32b-6bfa-4403-97bd-288dfe808239.png" alt=" Uploaded PDF displayed in the preview area with page navigation controls." style="display:block;margin:0 auto" width="1256" height="515" loading="lazy">

<h3 id="heading-creating-page-navigation">Creating Page Navigation</h3>
<p>Most PDF documents contain multiple pages.</p>
<p>Allow users to move between pages using Previous and Next buttons.</p>
<pre><code class="language-javascript">let currentPage = 1;

let pdfDocument = null;
</code></pre>
<p>Move to the previous page.</p>
<pre><code class="language-javascript">previousButton
.addEventListener(
    "click",
    async () =&gt; {

        if (
            currentPage === 1
        ) {

            return;

        }

        currentPage--;

        await renderPage(
            currentPage
        );

    }
);
</code></pre>
<p>Move to the next page.</p>
<pre><code class="language-javascript">nextButton
.addEventListener(
    "click",
    async () =&gt; {

        if (
            currentPage ===
            pdfDocument.numPages
        ) {

            return;

        }

        currentPage++;

        await renderPage(
            currentPage
        );

    }
);
</code></pre>
<p>Update the page counter whenever the current page changes.</p>
<pre><code class="language-javascript">pageIndicator.textContent =
    `Page ${currentPage} of ${pdfDocument.numPages}`;
</code></pre>
<p>This provides users with clear feedback while navigating large PDF documents.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/c99410bc-7d7c-411f-a8f1-8fd9b1aaf3b8.png" alt=" PDF preview with Previous and Next buttons for navigating between pages." style="display:block;margin:0 auto" width="546" height="818" loading="lazy">

<h3 id="heading-preparing-for-blur-editing">Preparing for Blur Editing</h3>
<p>Once the current page is rendered, the application prepares a transparent layer above the PDF canvas.</p>
<p>This overlay captures mouse interactions without modifying the original page preview.</p>
<p>Create the overlay.</p>
<pre><code class="language-html">&lt;canvas
    id="overlayCanvas"&gt;
&lt;/canvas&gt;
</code></pre>
<p>Match its size to the PDF preview.</p>
<pre><code class="language-javascript">overlayCanvas.width =
    pdfCanvas.width;

overlayCanvas.height =
    pdfCanvas.height;
</code></pre>
<p>Later in the tutorial, this overlay will allow users to draw blur regions while keeping the underlying PDF page untouched.</p>
<h3 id="heading-showing-the-editor">Showing the Editor</h3>
<p>After the first page finishes rendering, switch from the upload screen to the editor interface.</p>
<pre><code class="language-javascript">uploadSection.hidden =
    true;

editorSection.hidden =
    false;
</code></pre>
<p>Users can now preview the document, navigate between pages, and begin selecting areas that should be blurred.</p>
<h2 id="heading-creating-blur-regions">Creating Blur Regions</h2>
<p>Now that the PDF preview is working, we can build the most important feature of the application: allowing users to blur sensitive information.</p>
<p>Instead of editing the PDF immediately, users first draw one or more blur regions over the page preview.</p>
<p>Each region stores its own position, size, and blur intensity. These regions are later converted into actual PDF coordinates during final processing.</p>
<h3 id="heading-creating-the-blur-area-object">Creating the Blur Area Object</h3>
<p>Every blur region is represented as a JavaScript object.</p>
<p>For example:</p>
<pre><code class="language-javascript">const blurArea = {

    page: currentPage,

    x: 0,

    y: 0,

    width: 0,

    height: 0,

    intensity: 6

};
</code></pre>
<p>Rather than modifying the PDF immediately, the application simply keeps track of these objects until the user clicks <strong>Apply &amp; Finalize</strong>.</p>
<h3 id="heading-storing-multiple-blur-regions">Storing Multiple Blur Regions</h3>
<p>Users often need to hide more than one piece of information.</p>
<p>Store all blur areas inside an array.</p>
<pre><code class="language-javascript">const blurAreas = [];
</code></pre>
<p>Whenever a new blur box is created, push it into the array.</p>
<pre><code class="language-javascript">blurAreas.push({

    page: currentPage,

    x,

    y,

    width,

    height,

    intensity:
        blurIntensity

});
</code></pre>
<p>This makes it easy to redraw, edit, or remove individual blur regions later.</p>
<h3 id="heading-starting-a-blur-selection">Starting a Blur Selection</h3>
<p>The transparent overlay canvas captures mouse interactions.</p>
<p>When the user presses the mouse button, record the starting position.</p>
<pre><code class="language-javascript">let isDrawing = false;

let startX = 0;

let startY = 0;

overlayCanvas
.addEventListener(
    "mousedown",
    event =&gt; {

        isDrawing = true;

        startX = event.offsetX;

        startY = event.offsetY;

    }
);
</code></pre>
<p>The blur rectangle begins at this point.</p>
<h3 id="heading-drawing-the-blur-rectangle">Drawing the Blur Rectangle</h3>
<p>As the mouse moves, update the rectangle dimensions.</p>
<pre><code class="language-javascript">overlayCanvas
.addEventListener(
    "mousemove",
    event =&gt; {

        if (
            !isDrawing
        ) {

            return;

        }

        drawPreviewBox(

            startX,

            startY,

            event.offsetX,

            event.offsetY

        );

    }
);
</code></pre>
<p>The preview updates continuously while the user drags the mouse.</p>
<h3 id="heading-finishing-the-selection">Finishing the Selection</h3>
<p>When the mouse button is released, save the completed blur region.</p>
<pre><code class="language-javascript">overlayCanvas
.addEventListener(
    "mouseup",
    event =&gt; {

        isDrawing = false;

        blurAreas.push({

            page:
                currentPage,

            x:
                startX,

            y:
                startY,

            width:
                event.offsetX -
                startX,

            height:
                event.offsetY -
                startY,

            intensity:
                blurIntensity

        });

        redrawBlurAreas();

        updateBlurList();

    }
);
</code></pre>
<p>Each blur region becomes part of the current editing session.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/31655d91-dcb6-412c-8762-c6aedc732b81.png" alt="User dragging a blur rectangle over sensitive information in the PDF preview." style="display:block;margin:0 auto" width="565" height="863" loading="lazy">

<h3 id="heading-drawing-existing-blur-areas">Drawing Existing Blur Areas</h3>
<p>Whenever the page changes or a blur region is added, redraw every blur box.</p>
<pre><code class="language-javascript">function redrawBlurAreas() {

    overlayContext.clearRect(

        0,

        0,

        overlayCanvas.width,

        overlayCanvas.height

    );

    blurAreas

        .filter(

            area =&gt;

                area.page ===
                currentPage

        )

        .forEach(

            drawBlurArea

        );

}
</code></pre>
<p>This ensures that previously created blur regions remain visible while editing.</p>
<h3 id="heading-displaying-blur-boxes">Displaying Blur Boxes</h3>
<p>Render every stored region with a dashed outline.</p>
<pre><code class="language-javascript">function drawBlurArea(
    area
) {

    overlayContext
        .setLineDash([6, 4]);

    overlayContext
        .strokeStyle =
        "#4f6cff";

    overlayContext
        .strokeRect(

            area.x,

            area.y,

            area.width,

            area.height

        );

}
</code></pre>
<p>The outline acts as a guide and doesn't become part of the final PDF.</p>
<h3 id="heading-blur-options">Blur Options</h3>
<p>Users can choose how the blur should be applied.</p>
<p>The tool supports two modes:</p>
<ul>
<li><p>Blur selected areas</p>
</li>
<li><p>Blur entire page(s)</p>
</li>
</ul>
<p>The selected option controls the editing behavior.</p>
<pre><code class="language-javascript">const blurMode =
document.querySelector(

    'input[name="blurMode"]:checked'

).value;
</code></pre>
<p>If <strong>Blur selected areas</strong> is chosen, users draw blur rectangles manually.</p>
<p>If <strong>Blur entire page(s)</strong> is selected, the application skips manual selection and prepares to blur the entire page during final processing.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/54004f00-c974-4d77-b385-fe3136acaf04.png" alt="Blur options showing choices for blurring selected regions or entire PDF pages." style="display:block;margin:0 auto" width="642" height="231" loading="lazy">

<h3 id="heading-adjusting-blur-intensity">Adjusting Blur Intensity</h3>
<p>Different documents require different levels of blur.</p>
<p>A slider lets users control the blur strength before applying the effect.</p>
<pre><code class="language-javascript">const blurSlider =
document.getElementById(
    "blurIntensity"
);

let blurIntensity = 6;

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

        blurIntensity =
        Number(
            event.target.value
        );

    }
);
</code></pre>
<p>The selected value is stored with every newly created blur region.</p>
<pre><code class="language-javascript">blurArea.intensity =
blurIntensity;
</code></pre>
<p>Higher values produce a stronger blur effect.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/097cba2c-a441-401c-9f1c-f8ce0b5a26a1.png" alt="Blur intensity slider allowing users to adjust the strength of the blur effect." style="display:block;margin:0 auto" width="861" height="131" loading="lazy">

<h2 id="heading-managing-multiple-blur-areas">Managing Multiple Blur Areas</h2>
<p>Many documents contain several pieces of confidential information.</p>
<p>Instead of limiting users to a single blur rectangle, the application displays every saved region.</p>
<p>For example:</p>
<pre><code class="language-text">Blur Area #1

Blur Area #2

Blur Area #3
</code></pre>
<p>Each entry includes a remove button.</p>
<pre><code class="language-javascript">function removeBlurArea(
    index
) {

    blurAreas.splice(
        index,
        1
    );

    redrawBlurAreas();

    updateBlurList();

}
</code></pre>
<p>This allows users to delete only the blur region they no longer need.</p>
<p>To remove all blur regions from the current page:</p>
<pre><code class="language-javascript">function clearCurrentPage() {

    const remaining =

    blurAreas.filter(

        area =&gt;

            area.page !==
            currentPage

    );

    blurAreas.length = 0;

    blurAreas.push(
        ...remaining
    );

    redrawBlurAreas();

}
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/ef44e87e-7c39-4e2b-8e67-7e354853501d.png" alt="Blur area manager displaying multiple blur regions with delete controls and a Clear All on This Page button." style="display:block;margin:0 auto" width="876" height="273" loading="lazy">

<h2 id="heading-applying-blur-to-pages">Applying Blur to Pages</h2>
<p>Users may want to blur only one page or several pages within a document.</p>
<p>The editor provides three options:</p>
<ul>
<li><p>Current page only</p>
</li>
<li><p>All pages</p>
</li>
<li><p>Specific pages</p>
</li>
</ul>
<pre><code class="language-javascript">const pageOption =
document.querySelector(

'input[name="pageOption"]:checked'

).value;
</code></pre>
<p>If the user selects <strong>Specific pages</strong>, they can enter values such as:</p>
<pre><code class="language-text">1, 3-5, 8
</code></pre>
<p>These values will later be converted into an array of page numbers before the final PDF is generated.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/f3e1a72c-690d-4454-8b7a-50920817cd13.png" alt="Apply to Pages section with Current Page, All Pages, and Specific Pages options." style="display:block;margin:0 auto" width="566" height="292" loading="lazy">

<h3 id="heading-applying-the-blur-effect">Applying the Blur Effect</h3>
<p>So far, users have uploaded a PDF, selected one or more blur regions, adjusted the blur intensity, and chosen which pages should be processed.</p>
<p>The final step is converting those blur regions into actual blurred content inside the generated PDF.</p>
<p>Rather than modifying the original document directly, the application creates a new PDF while preserving the original file.</p>
<h3 id="heading-loading-the-original-pdf">Loading the Original PDF</h3>
<p>Start by loading the uploaded PDF into PDF-lib.</p>
<pre><code class="language-javascript">async function applyBlur() {

    const pdfDoc =

        await PDFLib.PDFDocument.load(
            originalPdfBytes.slice()
        );

    const pages =
        pdfDoc.getPages();

}
</code></pre>
<p>Using a copy of the original bytes ensures that the uploaded document remains unchanged.</p>
<h3 id="heading-processing-the-selected-pages">Processing the Selected Pages</h3>
<p>Determine which pages should receive the blur effect.</p>
<pre><code class="language-javascript">const selectedPages =

parsePageSelection(

    pageSelection,

    pdfDoc.getPageCount()

);
</code></pre>
<p>For example:</p>
<pre><code class="language-text">Current Page

↓

[2]


All Pages

↓

[1,2,3,4]


Specific Pages

↓

[1,3,5]
</code></pre>
<p>Only these pages will be modified during processing.</p>
<h3 id="heading-rendering-each-page-as-an-image">Rendering Each Page as an Image</h3>
<p>Since blur is a pixel-based effect, each selected PDF page is rendered into an off-screen canvas.</p>
<pre><code class="language-javascript">const page =

await pdfDocument.getPage(
    pageNumber
);

const viewport =
page.getViewport({

    scale: 2

});

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

canvas.width =
viewport.width;

canvas.height =
viewport.height;
</code></pre>
<p>Render the page.</p>
<pre><code class="language-javascript">await page.render({

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

    viewport

}).promise;
</code></pre>
<p>The canvas now contains a bitmap version of the PDF page that can be edited.</p>
<h3 id="heading-applying-blur-to-selected-regions">Applying Blur to Selected Regions</h3>
<p>Retrieve all blur regions that belong to the current page.</p>
<pre><code class="language-javascript">const pageRegions =

blurAreas.filter(

    area =&gt;

        area.page === pageNumber

);
</code></pre>
<p>Loop through every blur region.</p>
<pre><code class="language-javascript">pageRegions.forEach(

    area =&gt; {

        blurCanvasRegion(

            canvas,

            area

        );

    }

);
</code></pre>
<p>Each region is blurred independently.</p>
<h3 id="heading-blurring-the-canvas-region">Blurring the Canvas Region</h3>
<p>The browser's Canvas API allows filters to be applied while drawing.</p>
<p>Set the blur filter based on the selected intensity.</p>
<pre><code class="language-javascript">context.filter =

`blur(${area.intensity}px)`;
</code></pre>
<p>Redraw only the selected region.</p>
<pre><code class="language-javascript">context.drawImage(

    canvas,

    area.x,

    area.y,

    area.width,

    area.height,

    area.x,

    area.y,

    area.width,

    area.height

);
</code></pre>
<p>After drawing, reset the filter.</p>
<pre><code class="language-javascript">context.filter = "none";
</code></pre>
<p>Only the selected rectangle becomes blurred while the rest of the page remains unchanged.</p>
<h3 id="heading-blurring-an-entire-page">Blurring an Entire Page</h3>
<p>If the user chooses <strong>Blur entire page(s)</strong>, the process is much simpler.</p>
<p>Apply the filter to the full canvas.</p>
<pre><code class="language-javascript">context.filter =

`blur(${blurIntensity}px)`;

context.drawImage(

    canvas,

    0,

    0

);

context.filter =
"none";
</code></pre>
<p>The entire rendered page receives the selected blur effect.</p>
<h3 id="heading-converting-the-canvas-back-into-a-pdf-image">Converting the Canvas Back into a PDF Image</h3>
<p>After editing the canvas, convert it into an image.</p>
<pre><code class="language-javascript">const imageData =

canvas.toDataURL(
    "image/png"
);
</code></pre>
<p>Convert the image into bytes.</p>
<pre><code class="language-javascript">const bytes =

await fetch(imageData)

.then(

response =&gt;

response.arrayBuffer()

);
</code></pre>
<p>Embed the image inside PDF-lib.</p>
<pre><code class="language-javascript">const image =

await pdfDoc.embedPng(
    bytes
);
</code></pre>
<p>Replace the page contents.</p>
<pre><code class="language-javascript">const pdfPage =

pages[
pageNumber - 1
];

const size =
pdfPage.getSize();

pdfPage.drawImage(

    image,

    {

        x: 0,

        y: 0,

        width:
        size.width,

        height:
        size.height

    }

);
</code></pre>
<p>Repeat the same process for every selected page.</p>
<h3 id="heading-showing-the-processing-state">Showing the Processing State</h3>
<p>Generating large PDF files may take a few seconds.</p>
<p>Display a loading state while processing.</p>
<pre><code class="language-javascript">applyButton.disabled =
true;

applyButton.textContent =
"Applying...";
</code></pre>
<p>After processing finishes:</p>
<pre><code class="language-javascript">applyButton.disabled =
false;

applyButton.textContent =
"Apply &amp; Finalize";
</code></pre>
<p>This gives users clear feedback that the application is working.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/75e1e622-9769-4c17-8f39-49376de81041.png" alt="Apply &amp; Finalize button used to generate the blurred PDF." style="display:block;margin:0 auto" width="582" height="92" loading="lazy">

<p>During processing:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/68ae927f-aa3f-4e8c-8b37-2ef89ba0ad40.png" alt="Applying state displayed while the PDF blur operation is being completed." style="display:block;margin:0 auto" width="285" height="113" loading="lazy">

<h2 id="heading-generating-the-final-pdf">Generating the Final PDF</h2>
<p>After every page has been processed, save the completed document.</p>
<pre><code class="language-javascript">const pdfBytes =

await pdfDoc.save();

const outputBlob =
new Blob(

[pdfBytes],

{

type:
"application/pdf"

}

);
</code></pre>
<p>Store the result so it can be previewed and downloaded later.</p>
<pre><code class="language-javascript">generatedPdfBlob =
outputBlob;
</code></pre>
<p>At this point, the blurred PDF has been successfully generated.</p>
<h2 id="heading-previewing-the-result">Previewing the Result</h2>
<p>Hide the editing interface and display the completed document.</p>
<pre><code class="language-javascript">editorSection.hidden =
true;

resultSection.hidden =
false;
</code></pre>
<p>The result screen displays:</p>
<ul>
<li><p>Final PDF preview</p>
</li>
<li><p>Editable filename</p>
</li>
<li><p>Total pages</p>
</li>
<li><p>File size</p>
</li>
<li><p>Download button</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/1e96e1ec-b114-4ebb-955e-1e277d7f98da.png" alt="Final PDF preview showing multiple blurred regions with download options displayed beside the document." style="display:block;margin:0 auto" width="857" height="816" loading="lazy">

<p>The user can review the processed document before downloading it.</p>
<h2 id="heading-renaming-and-downloading">Renaming and Downloading</h2>
<p>Before downloading, users may want to rename the generated file.</p>
<p>Create a filename field.</p>
<pre><code class="language-html">&lt;input

type="text"

id="outputFilename"

value="blurred-document.pdf"&gt;
</code></pre>
<p>Validate the filename.</p>
<pre><code class="language-javascript">function getFilename() {

    let filename =
    outputFilename.value.trim();

    if (!filename) {

        filename =
        "blurred-document.pdf";

    }

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

        filename += ".pdf";

    }

    return filename;

}
</code></pre>
<p>Display additional file information.</p>
<pre><code class="language-javascript">pageCount.textContent =

`Pages:
${finalPdfDocument.numPages}`;

fileSize.textContent =

formatFileSize(
generatedPdfBlob.size
);
</code></pre>
<p>Download the processed document.</p>
<pre><code class="language-javascript">downloadButton
.addEventListener(

"click",

() =&gt; {

    const url =

    URL.createObjectURL(
        generatedPdfBlob
    );

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

    link.href = url;

    link.download =
    getFilename();

    link.click();

    URL.revokeObjectURL(
        url
    );

});
</code></pre>
<p>The browser downloads the completed PDF without sending any files to a remote server.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/1cd1538e-7a90-4994-bbd0-da84365e1d34.png" alt="Download section showing editable filename, page count, file size, and Download button." style="display:block;margin:0 auto" width="272" height="166" loading="lazy">

<p><img src="align=%22center%22" alt="align=%22center%22" width="600" height="400" loading="lazy"></p>
<h2 id="heading-demo-how-the-pdf-blur-tool-works">Demo: How the PDF Blur Tool Works</h2>
<p>Let's walk through the complete workflow.</p>
<h3 id="heading-step-1-upload-the-pdf">Step 1: Upload the PDF</h3>
<p>Users upload a PDF using drag-and-drop or the <strong>Select PDF</strong> button.</p>
<p>The browser validates the file and prepares it for rendering.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/9a08c61c-814a-4c44-9a74-6f4eef7ddefc.png" alt="Upload screen for selecting a PDF file." style="display:block;margin:0 auto" width="1256" height="515" loading="lazy">

<h3 id="heading-step-2-preview-the-document">Step 2: Preview the Document</h3>
<p>The uploaded PDF appears inside the preview window.</p>
<p>Users can move through the document using the page navigation controls.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/27024335-d786-476c-9c34-e050ca4162e7.png" alt="PDF preview with Previous and Next page navigation." style="display:block;margin:0 auto" width="546" height="818" loading="lazy">

<h3 id="heading-step-3-configure-blur-settings">Step 3: Configure Blur Settings</h3>
<p>Users choose whether to blur selected areas or entire pages.</p>
<p>They can also configure the blur intensity before creating any blur regions.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/6fdcce22-b42d-48ca-94cd-c99bc679b7f5.png" alt="Blur settings panel showing available blur options." style="display:block;margin:0 auto" width="400" height="758" loading="lazy">

<h3 id="heading-step-4-draw-blur-areas">Step 4: Draw Blur Areas</h3>
<p>Users click and drag directly on the PDF preview to create blur rectangles over sensitive content.</p>
<p>Multiple blur regions can be created on the same page.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/c5404708-118c-4f7f-a768-c20ea66626a7.png" alt="User creating blur rectangles over confidential information." style="display:block;margin:0 auto" width="565" height="863" loading="lazy">

<h3 id="heading-step-5-adjust-blur-intensity">Step 5: Adjust Blur Intensity</h3>
<p>The blur intensity slider controls how strong the blur effect should appear.</p>
<p>Higher values produce a stronger blur.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/b485fa69-8952-4e4a-b91c-945c9879838a.png" alt="blur seleted option" style="display:block;margin:0 auto" width="642" height="231" loading="lazy">

<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/d0ece3b0-2c75-45a2-9286-8a38a0ef706a.png" alt="Blur intensity slider controlling the strength of the blur effect." style="display:block;margin:0 auto" width="861" height="131" loading="lazy">

<h3 id="heading-step-6-manage-blur-regions">Step 6: Manage Blur Regions</h3>
<p>Individual blur areas can be removed, or all blur regions on the current page can be cleared.</p>
<p>This makes editing much easier before generating the final PDF.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/c90a4b21-9cfe-4c99-9b11-29b862621ca1.png" alt="Blur area management panel with multiple blur regions." style="display:block;margin:0 auto" width="876" height="273" loading="lazy">

<h3 id="heading-step-7-choose-the-pages">Step 7: Choose the Pages</h3>
<p>Users decide whether the blur should be applied to:</p>
<ul>
<li><p>Current page</p>
</li>
<li><p>All pages</p>
</li>
<li><p>Specific pages</p>
</li>
</ul>
<p>For example:</p>
<pre><code class="language-text">1,3-5,8
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/663facb2-f312-43a0-b4c8-4a029da0ed81.png" alt="Apply to Pages section with Current Page, All Pages, and Specific Pages options." style="display:block;margin:0 auto" width="566" height="292" loading="lazy">

<h3 id="heading-step-8-apply-the-blur">Step 8: Apply the Blur</h3>
<p>After reviewing the settings, users click <strong>Apply &amp; Finalize</strong>.</p>
<p>The application generates a new PDF containing the selected blur effects.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/53eb6910-25ec-4b66-aaf1-a0c11363a3f8.png" alt="Apply &amp; Finalize button generating the blurred PDF." style="display:block;margin:0 auto" width="582" height="92" loading="lazy">

<h3 id="heading-step-9-review-the-final-document">Step 9: Review the Final Document</h3>
<p>The completed PDF appears in the preview window.</p>
<p>Users can verify every blurred region before downloading.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/834a4f6d-e12a-45ae-b977-e761f624bb34.png" alt=" Final blurred PDF preview before downloading." style="display:block;margin:0 auto" width="857" height="816" loading="lazy">

<h3 id="heading-step-10-rename-and-download">Step 10: Rename and Download</h3>
<p>Finally, users rename the output file if needed and click <strong>Download</strong>.</p>
<p>The browser saves the completed PDF locally.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/573a6966-d0af-47af-a4d3-2c99b9a79622.png" alt=" Download section showing filename editing and Download button." style="display:block;margin:0 auto" width="272" height="166" loading="lazy">

<h2 id="heading-performance-tips">Performance Tips</h2>
<p>Large PDF files can take longer to render and process, but a few simple optimizations can keep the editor responsive.</p>
<p>Render only the page the user is currently viewing instead of loading the entire document.</p>
<pre><code class="language-javascript">await renderPage(
    currentPage
);
</code></pre>
<p>Reuse the same canvas and redraw only the blur regions when changes are made.</p>
<pre><code class="language-javascript">overlayContext.clearRect(
    0,
    0,
    overlayCanvas.width,
    overlayCanvas.height
);

redrawBlurAreas();
</code></pre>
<p>During final processing, generate only the pages selected by the user.</p>
<pre><code class="language-javascript">for (const page of selectedPages) {

    await processPage(page);

}
</code></pre>
<p>Finally, release temporary resources after the download completes.</p>
<pre><code class="language-javascript">URL.revokeObjectURL(
    downloadUrl
);
</code></pre>
<p>These optimizations reduce memory usage and help the PDF Blur Tool perform smoothly, even with large multi-page documents.</p>
<h2 id="heading-common-mistakes">Common Mistakes</h2>
<p>One common issue is storing blur coordinates before accounting for the current zoom level.</p>
<p>Always convert preview coordinates into the PDF's coordinate system before generating the final document.</p>
<pre><code class="language-javascript">const scaleX =

pdfWidth /
canvas.width;

const scaleY =

pdfHeight /
canvas.height;
</code></pre>
<p>Another mistake is allowing blur regions to extend beyond the page boundaries.</p>
<p>Clamp the values before processing.</p>
<pre><code class="language-javascript">blurArea.x = Math.max(
0,
blurArea.x
);

blurArea.y = Math.max(
0,
blurArea.y
);
</code></pre>
<p>Users should also verify the final preview before downloading, especially when multiple blur regions exist across different pages.</p>
<p>Finally, remember that this project applies a <strong>visual blur effect</strong> to the rendered PDF pages. If your application requires permanent removal of sensitive content rather than visual obscuring, additional document-redaction techniques are needed.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this tutorial, you built a browser-based PDF Blur Tool using JavaScript.</p>
<p>You learned how to upload and preview PDF documents, navigate between pages, create and manage multiple blur regions, adjust blur intensity, apply blur to selected pages, generate a new PDF with PDF-lib, preview the processed document, and download the final file –&nbsp;all without uploading data to a server.</p>
<p>By combining PDF.js, the HTML Canvas API, and PDF-lib, you created a privacy-focused PDF editing tool that runs entirely inside the browser.</p>
<p>You can explore the complete workflow using the <a href="https://allinonetools.net/blur-pdf/">PDF Blur Tool</a>.</p>
<p>From here, you could extend the project with features such as movable and resizable blur regions, undo and redo support, reusable blur presets, keyboard shortcuts, touch-device editing, or additional annotation tools for even more advanced browser-based PDF editing.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build an AI Agent with Function Calling in Node.js Using Google Gemini ]]>
                </title>
                <description>
                    <![CDATA[ Last year, a client asked me to add a conversational interface to their internal reporting tool. Staff would type a question, and the system would pull a live answer from the database. I had the first ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-ai-agent-function-calling-nodejs-gemini/</link>
                <guid isPermaLink="false">6a63af5c9a1ab0289b0cbb6a</guid>
                
                    <category>
                        <![CDATA[ Node.js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Tutorial ]]>
                    </category>
                
                    <category>
                        <![CDATA[ gemini ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Zia Ullah ]]>
                </dc:creator>
                <pubDate>Fri, 24 Jul 2026 18:30:52 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/ef2a5058-558c-4951-abff-4d51f1c5cd15.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Last year, a client asked me to add a conversational interface to their internal reporting tool. Staff would type a question, and the system would pull a live answer from the database.</p>
<p>I had the first version running in a day. Single questions worked. But a week in, a tester typed: "What is the weather in Berlin, and how much would 500 EUR convert to in USD right now?"</p>
<p>The model called the weather function, returned that answer, and ignored the second half of the question entirely.</p>
<p>That is the gap between a chatbot and an agent. A chatbot works from training data. That's its limit. An agent doesn't have that limit. It calls a tool, reads what came back, and decides whether to keep going.</p>
<p>Most questions resolve in one or two tool calls. Multi-step ones take a few more. Remove that loop, and they all break.</p>
<p>This tutorial shows you how to build that loop with Google Gemini's function calling API and Node.js. You'll build an agent that can call real external tools: Open-Meteo for live weather, frankfurter.app for live currency rates, and a math evaluator for calculations. All three are completely free. The only API key you need is Gemini, which is also free on Google AI Studio at 1,500 requests per day.</p>
<p>Everything is on GitHub: <a href="https://github.com/ziaongit/nodejs-gemini-agent">github.com/ziaongit/nodejs-gemini-agent</a>.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-how-function-calling-works">How Function Calling Works</a></p>
</li>
<li><p><a href="#heading-what-were-building">What We're Building</a></p>
</li>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-project-setup">Project Setup</a></p>
</li>
<li><p><a href="#heading-defining-the-tools">Defining the Tools</a></p>
</li>
<li><p><a href="#heading-implementing-the-tool-functions">Implementing the Tool Functions</a></p>
</li>
<li><p><a href="#heading-building-the-agentic-loop">Building the Agentic Loop</a></p>
</li>
<li><p><a href="#heading-the-cli-entry-point">The CLI Entry Point</a></p>
</li>
<li><p><a href="#heading-adding-an-express-http-server">Adding an Express HTTP Server</a></p>
</li>
<li><p><a href="#heading-testing-the-agent">Testing the Agent</a></p>
</li>
<li><p><a href="#heading-troubleshooting">Troubleshooting</a></p>
</li>
<li><p><a href="#heading-what-to-build-next">What to Build Next</a></p>
</li>
</ul>
<h2 id="heading-how-function-calling-works">How Function Calling Works</h2>
<p>Most LLM tutorials show function calling as: define a function, the model calls it, done. That framing skips the part that actually matters.</p>
<p>The model doesn't call your function. It can't. What happens is more like a negotiation.</p>
<p>You send the model a message along with a list of tool descriptions. Each description is a JSON schema: the function name, what it does, and what arguments it needs. Gemini reads those at request time to decide which tool, if any, fits what the user asked.</p>
<p>Here's the part that surprises people. Gemini doesn't run your code. It sends back a structured object that says: call <code>get_weather</code>, <code>city = Berlin</code>. Your code picks that up, runs the actual function, and sends the result back. Gemini checks whether that's enough to answer. If not, it requests another tool.</p>
<p>That exchange is the loop:</p>
<pre><code class="language-plaintext">User message
      │
      ▼
Model + tool schemas
      │
      ▼
Response: functionCall?
      │
   YES │                          NO
      ▼                            ▼
Run the function(s)         Return text answer
      │
      ▼
Send result(s) back to model
      │
      └──── loop back ────────────┘
</code></pre>
<p>The loop keeps running until the model decides it has enough to answer. That's what allows it to chain calls: check the weather, see the temperature is above 25°C, then decide it should also fetch the exchange rate before answering.</p>
<p>There's one detail that trips people up the first time. When Gemini requests multiple tools in the same response, you run all of them and return all results in a single message. Returning them one at a time in separate messages breaks the model's turn-tracking and produces unreliable output.</p>
<h2 id="heading-what-were-building">What We're Building</h2>
<p>In this tutorial, we'll build an AI agent with three working tools:</p>
<ul>
<li><p><code>get_weather</code> — fetches current weather for any city via Open-Meteo (free, no API key)</p>
</li>
<li><p><code>calculate</code> — evaluates a math expression safely in JavaScript</p>
</li>
<li><p><code>get_exchange_rate</code> — fetches live currency rates via frankfurter.app (free, no API key)</p>
</li>
</ul>
<p>There are two ways to run it: a readline CLI for quick local testing, and an Express HTTP endpoint to wire into a real application.</p>
<p>Full tech stack:</p>
<ul>
<li><p><strong>Node.js 20</strong>: runtime (Node 18 minimum for native fetch)</p>
</li>
<li><p><strong>@google/generative-ai</strong>: official Gemini SDK</p>
</li>
<li><p><strong>dotenv</strong>: environment variable loading</p>
</li>
<li><p><strong>Express</strong>: HTTP server for the API endpoint</p>
</li>
<li><p><strong>Open-Meteo API</strong>: free weather and geocoding, no key required</p>
</li>
<li><p><strong>frankfurter.app</strong>: free currency exchange rates, no key required</p>
</li>
</ul>
<p>Architecture:</p>
<pre><code class="language-plaintext">┌─────────────────────────────────────────────────┐
│                   Client                         │
│         CLI (readline) / HTTP POST               │
└──────────────────────┬──────────────────────────┘
                       │  user message
                       ▼
┌─────────────────────────────────────────────────┐
│               agent.js — Agentic Loop            │
│                                                  │
│  1. Send message + tool schemas to Gemini        │
│  2. Receive response                             │
│  3. functionCalls() present?                     │
│      YES → execute tools in parallel             │
│            send all results back                 │
│            go to step 2                          │
│      NO  → return final text answer              │
└──────────────────────┬──────────────────────────┘
                       │  tool calls
                       ▼
┌─────────────────────────────────────────────────┐
│                  Tool Handlers                   │
│                                                  │
│  get_weather(city)                               │
│    └─► geocoding-api.open-meteo.com              │
│        api.open-meteo.com                        │
│                                                  │
│  calculate(expression)                           │
│    └─► JS safe evaluator (no external call)      │
│                                                  │
│  get_exchange_rate(from, to, amount?)            │
│    └─► api.frankfurter.app                       │
└─────────────────────────────────────────────────┘
</code></pre>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before you start, you should have:</p>
<ul>
<li><p>Node.js 18 or higher — run <code>node --version</code> to check</p>
</li>
<li><p>A Gemini API key from <a href="https://aistudio.google.com">aistudio.google.com</a> — free, no card. The free tier gives you 1,500 requests a day.</p>
</li>
<li><p>You should also know how <code>async/await</code> works in Node.js. That's about it.</p>
</li>
</ul>
<h2 id="heading-project-setup">Project Setup</h2>
<pre><code class="language-bash">mkdir nodejs-gemini-agent &amp;&amp; cd nodejs-gemini-agent
npm init -y
npm install @google/generative-ai dotenv express
mkdir src
</code></pre>
<p>Add a <code>.gitignore</code>, as you don't want <code>.env</code> in your repo:</p>
<pre><code class="language-plaintext">node_modules/
.env
</code></pre>
<p>Drop a <code>.env</code> at the root:</p>
<pre><code class="language-plaintext">GEMINI_API_KEY=your_api_key_here
PORT=3000

# Optional: override the default model (gemini-2.0-flash)
# Uncomment if you hit free-tier quota limits
# GEMINI_MODEL=gemini-2.0-flash-lite
</code></pre>
<p>Project structure:</p>
<pre><code class="language-plaintext">nodejs-gemini-agent/
├── src/
│   ├── tools.js        ← tool schemas for Gemini
│   ├── functions.js    ← actual implementations
│   ├── agent.js        ← the agentic loop
│   ├── index.js        ← CLI entry point
│   └── server.js       ← Express HTTP server
├── .env
├── .env.example
├── .gitignore
└── package.json
</code></pre>
<h2 id="heading-defining-the-tools">Defining the Tools</h2>
<p>Gemini can't see your code. It picks tools based entirely on the JSON schemas you pass in. Each schema has a name, a description, and the parameter definitions.</p>
<p>The description is what drives routing. Gemini reads it at request time to decide which tool fits the user's question. Focus on when to call the function, not just what it does.</p>
<pre><code class="language-js">// src/tools.js

const toolDefinitions = [
  {
    name: 'get_weather',
    description:
      'Get the current weather for a city. Returns temperature in Celsius, ' +
      'humidity percentage, and wind speed. Use this when the user asks about ' +
      'weather, temperature, or climate conditions in any location.',
    parameters: {
      type: 'OBJECT',
      properties: {
        city: {
          type: 'STRING',
          description: 'The city name, e.g. Tokyo, London, New York',
        },
      },
      required: ['city'],
    },
  },
  {
    name: 'calculate',
    description:
      'Evaluate a mathematical expression and return the numeric result. ' +
      'Use this for arithmetic, percentage calculations, or any numeric computation ' +
      'the user asks for. Do not guess at math — always call this tool.',
    parameters: {
      type: 'OBJECT',
      properties: {
        expression: {
          type: 'STRING',
          description:
            'A valid mathematical expression, e.g. "47.50 * 0.18" or "1500 / 12"',
        },
      },
      required: ['expression'],
    },
  },
  {
    name: 'get_exchange_rate',
    description:
      'Get the current exchange rate between two currencies. Can also convert ' +
      'a specific amount. Use this when the user asks about currency conversion, ' +
      'exchange rates, or how much a foreign currency amount is worth.',
    parameters: {
      type: 'OBJECT',
      properties: {
        from: {
          type: 'STRING',
          description: 'The source currency code, e.g. USD, EUR, JPY, GBP',
        },
        to: {
          type: 'STRING',
          description: 'The target currency code, e.g. USD, EUR, JPY, GBP',
        },
        amount: {
          type: 'NUMBER',
          description:
            'Amount to convert. Optional — defaults to 1 if not provided.',
        },
      },
      required: ['from', 'to'],
    },
  },
];

module.exports = { toolDefinitions };
</code></pre>
<p>Vague descriptions work most of the time. The problems show up at the edges. "Does currency stuff" routes fine on a simple question. It falls apart on anything ambiguous. "Get the current exchange rate between two currencies. Use this when the user asks about currency conversion." holds up. The extra words cost basically nothing. Debugging bad routing costs a lot more.</p>
<h2 id="heading-implementing-the-tool-functions">Implementing the Tool Functions</h2>
<p>These are the actual functions that run when the model requests them. Each receives the arguments the model decided to pass, does real work, and returns a plain JavaScript object.</p>
<pre><code class="language-js">// src/functions.js

async function get_weather({ city }) {
  // Open-Meteo uses a two-step approach: geocode the city first, then fetch weather.
  // Both APIs are free with no key required.
  const geoUrl = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(city)}&amp;count=1`;
  const geoRes  = await fetch(geoUrl);
  const geoData = await geoRes.json();

  if (!geoData.results?.length) {
    return { error: `City not found: ${city}` };
  }

  const { latitude, longitude, name, country } = geoData.results[0];

  const weatherUrl =
    `https://api.open-meteo.com/v1/forecast` +
    `?latitude=${latitude}&amp;longitude=${longitude}` +
    `&amp;current=temperature_2m,relative_humidity_2m,wind_speed_10m,weather_code`;

  const weatherRes  = await fetch(weatherUrl);
  const weatherData = await weatherRes.json();
  const current     = weatherData.current;

  return {
    city:        `${name}, ${country}`,
    temperature: `${current.temperature_2m}°C`,
    humidity:    `${current.relative_humidity_2m}%`,
    wind_speed:  `${current.wind_speed_10m} km/h`,
  };
}

function calculate({ expression }) {
  try {
    // Strip anything that is not a number or basic operator before eval.
    // This is not a complete sandbox — use a proper math parser like mathjs
    // in production if expressions come from untrusted users.
    const safe = expression.replace(/[^0-9+\-*/.() %]/g, '');
    if (!safe.trim()) return { error: 'Invalid or empty expression' };

    const result = Function('"use strict"; return (' + safe + ')')();
    return { expression, result };
  } catch {
    return { error: `Could not evaluate: ${expression}` };
  }
}

async function get_exchange_rate({ from, to, amount = 1 }) {
  const url  = `https://api.frankfurter.app/latest?from=${from.toUpperCase()}&amp;to=${to.toUpperCase()}`;
  const res  = await fetch(url);
  const data = await res.json();

  if (data.error) return { error: data.error };

  const rate      = data.rates[to.toUpperCase()];
  if (!rate) return { error: `No rate found for ${from} → ${to}` };

  const converted = parseFloat((amount * rate).toFixed(4));

  return { from: from.toUpperCase(), to: to.toUpperCase(), rate, amount, converted };
}

module.exports = { get_weather, calculate, get_exchange_rate };
</code></pre>
<p>There are a few things worth pointing out here.</p>
<p>Open-Meteo uses geocoding before the weather fetch. Passing latitude and longitude directly to the weather endpoint is more reliable than a city name string, and the geocoding API handles misspellings reasonably well. There are two fetch calls, but that's the trade-off for accuracy.</p>
<p>The <code>calculate</code> function strips everything except digits and operators before evaluation. It narrows the injection risk, but it's not a real sandbox. If you expose this over HTTP with anonymous users, use a proper math parser like <a href="https://mathjs.org">mathjs</a>.</p>
<p>Frankfurter converts currency codes to uppercase before the request. Users will type "usd" or "Usd" and both should work. The API is case-sensitive on its end.</p>
<h2 id="heading-building-the-agentic-loop">Building the Agentic Loop</h2>
<p>This is the file everything else supports. The loop itself is about 20 lines. The rest is logging and error handling.</p>
<pre><code class="language-js">// src/agent.js
const { GoogleGenerativeAI } = require('@google/generative-ai');
const { toolDefinitions }    = require('./tools');
const { get_weather, calculate, get_exchange_rate } = require('./functions');

const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY);

// Map tool names to handler functions
const toolHandlers = { get_weather, calculate, get_exchange_rate };

async function runAgent(userMessage) {
  const model = genAI.getGenerativeModel({
    model: process.env.GEMINI_MODEL || 'gemini-2.0-flash',
    tools: [{ functionDeclarations: toolDefinitions }],
  });

  const chat = model.startChat();

  console.log(`\nUser: ${userMessage}`);
  console.log('---');

  let response = await chat.sendMessage(userMessage);
  let iterations = 0;
  const MAX_ITERATIONS = 10; // safety cap against infinite loops

  // Agentic loop
  while (iterations &lt; MAX_ITERATIONS) {
    iterations++;
    const calls = response.response.functionCalls();

    // No tool calls — model is done, return the answer
    if (!calls || calls.length === 0) break;

    // Run all requested tools, collect results
    const toolResults = await Promise.allSettled(
      calls.map(async (call) =&gt; {
        console.log(`Calling tool: ${call.name}(${JSON.stringify(call.args)})`);

        const handler = toolHandlers[call.name];

        if (!handler) {
          return {
            functionResponse: {
              name:     call.name,
              response: { error: `Unknown tool: ${call.name}` },
            },
          };
        }

        try {
          const result = await handler(call.args);
          console.log(`Tool result: ${JSON.stringify(result)}`);
          return {
            functionResponse: {
              name:     call.name,
              response: result,
            },
          };
        } catch (err) {
          return {
            functionResponse: {
              name:     call.name,
              response: { error: err.message },
            },
          };
        }
      })
    );

    // Extract values from allSettled (fulfilled only — errors already caught above)
    const parts = toolResults
      .filter(r =&gt; r.status === 'fulfilled')
      .map(r =&gt; r.value);

    // Send all results back to the model in one message
    response = await chat.sendMessage(parts);
  }

  return response.response.text();
}

module.exports = { runAgent };
</code></pre>
<p>The <code>MAX_ITERATIONS</code> cap isn't paranoia. A model can get into a loop if a tool keeps returning an error and the model keeps retrying. 10 iterations is more than enough for any real query. A complex multi-tool question typically resolves in two or three turns.</p>
<p><code>Promise.allSettled</code> runs all requested tools in parallel rather than sequentially. When the model requests both weather and exchange rate in the same response, they fetch simultaneously. Individual tool failures get caught inside the map rather than letting one failure abort the others.</p>
<p>The logging is intentional. When you're building and testing an agent, watching the tool calls happen in real time tells you whether the routing is working. Production code would route these to a structured logger instead of console output, but the information is the same.</p>
<h2 id="heading-the-cli-entry-point">The CLI Entry Point</h2>
<p><code>require('dotenv').config()</code> runs first so your API key loads before anything else. After that it's a standard readline loop: each line you type goes to <code>runAgent</code>, the response prints, and the loop waits for the next input.</p>
<pre><code class="language-js">// src/index.js
require('dotenv').config();
const readline     = require('readline');
const { runAgent } = require('./agent');

const rl = readline.createInterface({
  input:  process.stdin,
  output: process.stdout,
});

function ask(prompt) {
  return new Promise(resolve =&gt; rl.question(prompt, resolve));
}

async function main() {
  console.log('Gemini Agent — type your question, or "exit" to quit\n');

  while (true) {
    const input = await ask('You: ');
    if (input.toLowerCase() === 'exit') break;
    if (!input.trim()) continue;

    try {
      const answer = await runAgent(input);
      console.log(`\nAgent: ${answer}\n`);
    } catch (err) {
      console.error(`Error: ${err.message}`);
    }
  }

  rl.close();
}

main();
</code></pre>
<h2 id="heading-adding-an-express-http-server">Adding an Express HTTP Server</h2>
<p>The CLI is useful for testing. For integrating into an app, you need an HTTP endpoint.</p>
<pre><code class="language-js">// src/server.js
require('dotenv').config();
const express      = require('express');
const { runAgent } = require('./agent');

const app  = express();
const PORT = process.env.PORT || 3000;

app.use(express.json());

app.post('/agent', async (req, res) =&gt; {
  const { message } = req.body;

  if (!message || typeof message !== 'string') {
    return res.status(400).json({ error: 'message is required and must be a string' });
  }

  try {
    const answer = await runAgent(message);
    res.json({ answer });
  } catch (err) {
    console.error('[agent error]', err.message);
    res.status(500).json({ error: 'Agent failed to process the request' });
  }
});

app.listen(PORT, () =&gt; {
  console.log(`Agent server running on http://localhost:${PORT}`);
});
</code></pre>
<p>Start it:</p>
<pre><code class="language-bash">node src/server.js
</code></pre>
<p>Call it:</p>
<pre><code class="language-bash">curl -X POST http://localhost:3000/agent \
  -H "Content-Type: application/json" \
  -d '{"message": "What is the weather in Paris?"}'
</code></pre>
<p>Response:</p>
<pre><code class="language-json">{
  "answer": "The current weather in Paris, France is 19°C with 65% humidity and wind speeds of 12 km/h."
}
</code></pre>
<p>The POST body is a plain <code>{ message }</code> object. The response is a plain <code>{ answer }</code> string. The agent handles the rest.</p>
<h2 id="heading-testing-the-agent">Testing the Agent</h2>
<p>Start the CLI:</p>
<pre><code class="language-bash">node src/index.js
</code></pre>
<p>The <code>You:</code> prompt comes from the readline in <code>index.js</code>. The <code>User:</code> line and <code>---</code> separator are logged by <code>agent.js</code> at the start of each run. This is the same logging discussed in the agentic loop section.</p>
<p>Single tool — weather:</p>
<pre><code class="language-plaintext">You: What's the weather in Tokyo?

User: What's the weather in Tokyo?
---
Calling tool: get_weather({"city":"Tokyo"})
Tool result: {"city":"Tokyo, JP","temperature":"31°C","humidity":"72%","wind_speed":"8 km/h"}

Agent: The current weather in Tokyo, Japan is 31°C with 72% humidity and wind speeds of 8 km/h.
</code></pre>
<p>Two tools — chained reasoning:</p>
<pre><code class="language-plaintext">You: What is the weather in Tokyo? If it's above 20°C, convert 10000 JPY to EUR.

User: What is the weather in Tokyo? If it's above 20°C, convert 10000 JPY to EUR.
---
Calling tool: get_weather({"city":"Tokyo"})
Tool result: {"city":"Tokyo, JP","temperature":"31°C","humidity":"72%","wind_speed":"8 km/h"}

Calling tool: get_exchange_rate({"from":"JPY","to":"EUR","amount":10000})
Tool result: {"from":"JPY","to":"EUR","rate":0.006,"amount":10000,"converted":60.0}

Agent: The current temperature in Tokyo is 31°C, which is above 20°C.
Converting 10,000 JPY to EUR at the current exchange rate gives approximately 60.00 EUR.
</code></pre>
<p>Notice what happened: the model called <code>get_weather</code>, read the result (31°C), applied the conditional logic from the user's question on its own, and then called <code>get_exchange_rate</code>. You didn't write any of that branching logic. The model handled it from the description alone.</p>
<p>Calculator:</p>
<pre><code class="language-plaintext">You: How much is 18% tip on a $47.50 restaurant bill?

User: How much is 18% tip on a $47.50 restaurant bill?
---
Calling tool: calculate({"expression":"47.50 * 0.18"})
Tool result: {"expression":"47.50 * 0.18","result":8.55}

Agent: An 18% tip on a $47.50 bill is $8.55, making your total $56.05.
</code></pre>
<p>Three tools in one query:</p>
<pre><code class="language-plaintext">You: What's the weather in London and Berlin? And what is 250 GBP in EUR?

User: What's the weather in London and Berlin? And what is 250 GBP in EUR?
---
Calling tool: get_weather({"city":"London"})
Calling tool: get_weather({"city":"Berlin"})
Calling tool: get_exchange_rate({"from":"GBP","to":"EUR","amount":250})
Tool result: {"city":"London, GB","temperature":"16°C","humidity":"78%","wind_speed":"20 km/h"}
Tool result: {"city":"Berlin, DE","temperature":"22°C","humidity":"55%","wind_speed":"14 km/h"}
Tool result: {"from":"GBP","to":"EUR","rate":1.17,"amount":250,"converted":292.5}

Agent: London is currently 16°C with 78% humidity and 20 km/h winds.
Berlin is warmer at 22°C with 55% humidity and lighter winds of 14 km/h.
250 GBP converts to approximately 292.50 EUR at the current exchange rate.
</code></pre>
<p>All three tools ran in parallel. <code>Promise.allSettled</code> is why. A sequential loop would have made three serial network requests. Parallel gives you the same result in roughly the time of the slowest single request.</p>
<h2 id="heading-troubleshooting">Troubleshooting</h2>
<p>Here are a few common issues you might encounter, and how to fix them:</p>
<h3 id="heading-1-404-not-found-modelsgemini-15-flash-is-not-found-for-api-version-v1beta">1. <code>[404 Not Found] models/gemini-1.5-flash is not found for API version v1beta</code></h3>
<p>The model name is outdated. Google deprecates older aliases over time. Swap it out for <code>gemini-2.0-flash</code> in <code>agent.js</code>. To check what models your key can actually access, run:</p>
<pre><code class="language-bash">node -e "
const { GoogleGenerativeAI } = require('@google/generative-ai');
require('dotenv').config();
const g = new GoogleGenerativeAI(process.env.GEMINI_API_KEY);
g.listModels().then(r =&gt; r.models.forEach(m =&gt; console.log(m.name)));
"
</code></pre>
<h3 id="heading-2-429-too-many-requests-you-exceeded-your-current-quota">2. <code>[429 Too Many Requests] You exceeded your current quota</code></h3>
<p>The <code>gemini-2.0-flash</code> free tier caps you at 1,500 requests a day. Hit that and every call returns a 429 until midnight Pacific resets the counter.</p>
<p>The error names the quota ID directly. <code>GenerateRequestsPerDayPerProjectPerModel-FreeTier</code> means you hit the daily cap. <code>GenerateRequestsPerMinutePerProjectPerModel-FreeTier</code> means the per-minute rate.</p>
<p>For the per-minute limit, the error includes a <code>retryDelay</code> field. Wait that many seconds and retry. For the daily limit, the quota is per-project. All models under the same project share it.</p>
<p>There are three ways out:</p>
<ul>
<li><p><strong>New project</strong> (fastest): head to <a href="https://aistudio.google.com">aistudio.google.com</a>, spin up a new project, grab a new API key, and swap it into <code>.env</code>. You get a fresh quota immediately.</p>
</li>
<li><p><strong>Enable billing</strong>: billing-enabled projects get much higher limits while keeping the free usage tier. Set up at <a href="https://aistudio.google.com">aistudio.google.com</a>.</p>
</li>
<li><p><strong>Wait</strong>: resets daily at midnight Pacific.</p>
</li>
</ul>
<p>Because <code>agent.js</code> reads the model name from <code>process.env.GEMINI_MODEL</code>, you can also switch models without touching code. Add this to your <code>.env</code> to test with a lighter model:</p>
<pre><code class="language-plaintext">GEMINI_MODEL=gemini-2.0-flash-lite
</code></pre>
<p>Remove the line when your quota resets and the agent goes back to <code>gemini-2.0-flash</code>.</p>
<h3 id="heading-3-error-geminiapikey-is-not-set">3. <code>Error: GEMINI_API_KEY is not set</code></h3>
<p>Nine times out of ten, <code>require('dotenv').config()</code> is either missing or buried below other requires. Drag it to the very top of <code>index.js</code>. Your <code>.env</code> also needs to live at the project root with your actual key in it, not <code>your_api_key_here</code>.</p>
<h3 id="heading-4-googlegenerativeaierror-400-invalidargument">4. <code>GoogleGenerativeAIError: 400 INVALID_ARGUMENT</code></h3>
<p>Almost always a malformed tool schema. Gemini uses uppercase type strings: <code>'OBJECT'</code>, <code>'STRING'</code>, <code>'NUMBER'</code>. JSON Schema uses lowercase. Check your <code>parameters.type</code> values.</p>
<h3 id="heading-5-model-answers-without-calling-any-tools">5. Model Answers Without Calling Any Tools</h3>
<p>The description is too vague or the user's question doesn't match well enough for the model to route it. Add more context to the description about when the tool should be used. The phrase "use this when the user asks about X" directly improves routing accuracy.</p>
<h3 id="heading-6-typeerror-fetch-is-not-a-function">6. <code>TypeError: fetch is not a function</code></h3>
<p>Node 17 and below don't have native <code>fetch</code>. It was added in Node 18. Run <code>node --version</code> to check yours.</p>
<p>If you can't upgrade, install it with <code>npm install node-fetch</code>. Every file that calls <code>fetch</code> then needs <code>const fetch = require('node-fetch')</code> as its first line.</p>
<h3 id="heading-7-tool-works-in-isolation-but-agent-loop-doesnt-call-it">7. Tool Works in Isolation but Agent Loop Doesn't Call it</h3>
<p>The name in <code>toolDefinitions</code> must exactly match the key in <code>toolHandlers</code>. Case matters in JavaScript. <code>get_Weather</code> and <code>get_weather</code> are two different things.</p>
<h3 id="heading-8-exchange-rate-returns-no-rate-found">8. Exchange Rate Returns <code>No rate found</code></h3>
<p>The currency code you passed isn't supported by frankfurter.app. The API covers ~30 major currencies. Check supported codes at <a href="https://www.frankfurter.app/docs/">frankfurter.app</a>.</p>
<h2 id="heading-what-to-build-next">What to Build Next</h2>
<p>The three tools here are a foundation. The loop works the same way regardless of how many tools you add.</p>
<p><strong>Database lookup tool:</strong> A <code>search_products</code> function that queries your PostgreSQL table turns the agent into a product assistant. Point it at your catalog, and it can answer questions about availability, pricing, and specs without you writing any routing logic.</p>
<p><strong>Write tools:</strong> <code>get_*</code> functions make the agent read-only. Add a <code>create_ticket</code> or <code>send_notification</code> function and the agent can take actions: file a support request, trigger a workflow, update a record. Once you add write tools, think carefully about <strong>which queries should require confirmation before executing</strong>.</p>
<p><strong>Memory across sessions:</strong> Right now <code>model.startChat()</code> creates a fresh conversation on every call. Pass a <code>history</code> array when starting the chat and the model remembers prior turns. Store that history in PostgreSQL or Redis keyed to the user ID, and the agent carries context across sessions.</p>
<p><strong>Streaming responses:</strong> For a UI that shows the answer as it types rather than waiting for the full response, replace <code>chat.sendMessage</code> with <code>chat.sendMessageStream</code>. The tool call loop stays the same. Only the final response delivery changes.</p>
<p><strong>Swap the model:</strong> The <code>model</code> string in <code>getGenerativeModel</code> is the only thing that pins you to Gemini 2.0 Flash. <code>gemini-2.0-flash-lite</code> is lighter and faster for simpler queries. For stronger reasoning on complex tasks, run the <code>listModels</code> script from the Troubleshooting section to find the latest available models. The function calling interface is identical across all Gemini models, so swapping takes one line.</p>
<p>The full source code for this article is on GitHub at <a href="https://github.com/ziaongit/nodejs-gemini-agent">github.com/ziaongit/nodejs-gemini-agent</a>.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Make a Static HTML Page Editable in the Browser with Vanilla JavaScript ]]>
                </title>
                <description>
                    <![CDATA[ When you maintain a document for someone else, such as a résumé, a one-page portfolio, or a printable menu, the bottleneck is rarely the layout. It's the edit loop. Every small change ("move this bull ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-make-a-static-html-page-editable-in-the-browser-with-vanilla-javascript/</link>
                <guid isPermaLink="false">6a61266b62f25d8178b980e0</guid>
                
                    <category>
                        <![CDATA[ HTML ]]>
                    </category>
                
                    <category>
                        <![CDATA[ CSS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ DOM ]]>
                    </category>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ timothy ogbemudia ]]>
                </dc:creator>
                <pubDate>Wed, 22 Jul 2026 20:22:03 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/08400ed2-ef6a-404f-a135-b6cefe9d6919.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>When you maintain a document for someone else, such as a résumé, a one-page portfolio, or a printable menu, the bottleneck is rarely the layout. It's the edit loop.</p>
<p>Every small change ("move this bullet up", "delete that line", "this link is dead") goes through you, the person with the code editor, even though the person requesting the change knows exactly what they want.</p>
<p>I ran into this maintaining a family member's résumé as a single static HTML file. The design was done. The content was theirs. But every revision, whether it was reordering a role, adding a certification, fixing a link, or nudging a print page break, meant another round of "send me the change, I'll edit the file." After the tenth round, the fix became obvious: make the page edit itself.</p>
<p>In this article, you'll build an in-browser editing layer for a static HTML page using <code>contenteditable</code>, about a hundred lines of vanilla JavaScript, and no build step. The person editing can change any text, reorder or delete any block, add new content, edit links, control print pagination, and print to PDF. A refresh restores the original file, untouched.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-what-you-will-learn">What You Will Learn</a></p>
</li>
<li><p><a href="#heading-what-is-contenteditable">What Iscontenteditable?</a></p>
</li>
<li><p><a href="#heading-why-not-a-react-app">Why Not a React App?</a></p>
</li>
<li><p><a href="#heading-how-to-prepare-the-markup-for-reordering">How to Prepare the Markup for Reordering</a></p>
</li>
<li><p><a href="#heading-how-to-attach-controls-to-every-block">How to Attach Controls to Every Block</a></p>
</li>
<li><p><a href="#heading-how-to-show-controls-only-on-the-innermost-block">How to Show Controls Only on the Innermost Block</a></p>
</li>
<li><p><a href="#heading-how-to-move-and-delete-blocks">How to Move and Delete Blocks</a></p>
</li>
<li><p><a href="#heading-how-to-edit-links-inside-contenteditable">How to Edit Links Insidecontenteditable</a></p>
</li>
<li><p><a href="#heading-how-to-let-users-control-print-pagination">How to Let Users Control Print Pagination</a></p>
</li>
<li><p><a href="#heading-how-to-add-new-content-from-templates">How to Add New Content from Templates</a></p>
</li>
<li><p><a href="#heading-why-nothing-persists">Why Nothing Persists</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To follow along, you should have:</p>
<ul>
<li><p>Working knowledge of HTML and CSS, including CSS Grid and <code>@media print</code></p>
</li>
<li><p>Basic understanding of JavaScript DOM APIs (<code>querySelector</code>, event listeners, creating elements)</p>
</li>
<li><p>No frameworks, libraries, or build tools. That's the point.</p>
</li>
</ul>
<h2 id="heading-what-you-will-learn">What You Will Learn</h2>
<ul>
<li><p>What <code>contenteditable</code> gives you for free, and where it stops</p>
</li>
<li><p>How to attach move/delete controls to repeatable blocks with one reusable function</p>
</li>
<li><p>How to show controls only on the innermost hovered block using <code>:has()</code></p>
</li>
<li><p>How to reorder DOM nodes without a framework, and the markup prep that makes it safe</p>
</li>
<li><p>How to edit link URLs inside an editable region</p>
</li>
<li><p>How to let users place print page breaks themselves</p>
</li>
<li><p>How to add new content from templates, with placeholder text pre-selected</p>
</li>
<li><p>Why "nothing persists" can be a feature, not a limitation</p>
</li>
</ul>
<h2 id="heading-what-is-contenteditable">What Is <code>contenteditable</code>?</h2>
<p><code>contenteditable</code> is an HTML attribute that turns any element into an editable region. The browser handles the hard parts: caret placement, text selection, typing, deletion, clipboard, and undo history.</p>
<pre><code class="language-html">&lt;div class="page" contenteditable="true" spellcheck="false"&gt;
  &lt;!-- the entire document --&gt;
&lt;/div&gt;
</code></pre>
<p>That one attribute gets you further than you might expect. Clicking any paragraph places a caret. Cmd+Z undoes typing. Pressing Enter inside a <code>&lt;ul&gt;</code> creates a new <code>&lt;li&gt;</code>. The browser understands list semantics natively, so "add a bullet by pressing Enter" works with zero code.</p>
<p>But <code>contenteditable</code> alone isn't an editor. It has no concept of <em>blocks</em>. It won't move a job entry above another one, delete a card cleanly, or change an <code>href</code>. Clicking a link inside an editable region just places the caret in its text. Everything structural is on you. The rest of this article is about filling that gap.</p>
<h2 id="heading-why-not-a-react-app">Why Not a React App?</h2>
<p>The obvious alternative is to rebuild the page as a "real" app: components, state, a form per section, and an export button. I decided against it, and the tradeoff is important.</p>
<p>The file lives in a <code>public/</code> folder and is served as a static asset. It works from a URL, from disk, or from an email attachment. It has no dependencies to install, no build to run, and no way to rot when a toolchain updates.</p>
<p>The editing needs are small and bounded: change text, move blocks, delete blocks, add blocks, and print. That's DOM manipulation, the thing the DOM API is already good at.</p>
<p>A framework earns its complexity when state outlives the DOM: persistence, collaboration, validation, and syncing. This page has none of those requirements. When your state <em>is</em> the DOM and the lifetime is one session, a framework is an extra layer that buys you nothing.</p>
<h2 id="heading-how-to-prepare-the-markup-for-reordering">How to Prepare the Markup for Reordering</h2>
<p>Before writing any JavaScript, look at your markup for anything positional, meaning elements that only make sense <em>between</em> other elements. In my case, jobs were separated by <code>&lt;hr&gt;</code> dividers:</p>
<pre><code class="language-html">&lt;div class="job"&gt;...&lt;/div&gt;
&lt;hr class="divider"&gt;
&lt;div class="job"&gt;...&lt;/div&gt;
</code></pre>
<p>The moment blocks can move or be deleted, separators like this become landmines. Delete a job and its divider survives as an orphaned line. Move a job and the divider stays behind.</p>
<p>The fix is to delete the <code>&lt;hr&gt;</code> elements entirely and derive the separator from adjacency:</p>
<pre><code class="language-css">.section .job + .job {
  border-top: 0.5px solid var(--rule);
  padding-top: 18px;
}
</code></pre>
<p>The sibling combinator draws a rule above every job that follows another job. Reorder them, delete them, add new ones, and the separators are always exactly where they should be, because they're computed from structure rather than stored in it. This is the same principle as deriving state instead of duplicating it, applied to CSS.</p>
<h2 id="heading-how-to-attach-controls-to-every-block">How to Attach Controls to Every Block</h2>
<p>Each movable block gets a small control cluster with move up, move down, and delete actions, injected by one reusable function:</p>
<pre><code class="language-js">const SELECTORS = ['.section', '.job', '.bullets li', '.cert-card', '.skills-row', '.edu-row'];
const BREAKABLE = new Set(['.section', '.job']);

function makeBlock(el, sel) {
  el.classList.add('blk');
  el.dataset.sel = sel;
  const ctl = document.createElement('span');
  ctl.className = 'ctl';
  ctl.setAttribute('contenteditable', 'false');
  ctl.innerHTML =
    '&lt;button data-act="up" data-tip="Move this up"&gt;↑&lt;/button&gt;' +
    '&lt;button data-act="down" data-tip="Move this down"&gt;↓&lt;/button&gt;' +
    (BREAKABLE.has(sel) ? '&lt;button data-act="brk" data-tip="Page break: start a new printed page here"&gt;⇟&lt;/button&gt;' : '') +
    '&lt;button data-act="del" data-tip="Remove this. Refresh to bring it back"&gt;×&lt;/button&gt;';
  el.appendChild(ctl);
}

SELECTORS.forEach(sel =&gt; {
  page.querySelectorAll(sel).forEach(el =&gt; makeBlock(el, sel));
});
</code></pre>
<p>A few design decisions are worth calling out.</p>
<p>First, <code>contenteditable="false"</code> on the control cluster. Editable regions inherit. Everything inside the page is editable unless you opt out. Without this, the user could place a caret inside your buttons and delete them like text.</p>
<p>Second, <code>el.dataset.sel</code> records <em>which selector matched</em>. This matters later: when a block moves, it should only swap with siblings of its own kind. A bullet moves among bullets, a job among jobs. Storing the selector on the element makes that check trivial.</p>
<p>Third, the controls live <em>inside</em> the block they control. That gives you positioning for free, with <code>position: absolute</code> against the block's own <code>position: relative</code>, and means a block carries its controls with it wherever it moves.</p>
<h2 id="heading-how-to-show-controls-only-on-the-innermost-block">How to Show Controls Only on the Innermost Block</h2>
<p>Blocks nest: a bullet sits inside a job, which sits inside a section. Hovering a bullet technically hovers all three, and naïve CSS shows three control clusters at once. The result is visual noise exactly where the user is trying to focus.</p>
<p>Modern CSS solves this in one line:</p>
<pre><code class="language-css">.blk:hover:not(:has(.blk:hover)) &gt; .ctl { display: inline-flex; }
</code></pre>
<p>Read it inside out: show a block's controls when it's hovered, <em>unless</em> some descendant block is also hovered, in which case that deeper block wins. Hover a bullet, you get bullet controls. Hover the job's title (outside any bullet), you get job controls. One rule, no JavaScript.</p>
<p><code>:has()</code> is supported in every current browser, but a fallback costs one more rule:</p>
<pre><code class="language-css">@supports not selector(:has(*)) {
  .blk:hover &gt; .ctl { display: inline-flex; }
}
</code></pre>
<p>Older browsers get the noisier all-ancestors behavior instead of no controls at all. Degrade loudly, not silently.</p>
<h2 id="heading-how-to-move-and-delete-blocks">How to Move and Delete Blocks</h2>
<p>With controls attached, the actual reordering is short. One delegated listener handles every button on the page:</p>
<pre><code class="language-js">function siblings(el) {
  return [...el.parentElement.children].filter(c =&gt;
    c.classList.contains('blk') &amp;&amp; c.dataset.sel === el.dataset.sel);
}

page.addEventListener('click', e =&gt; {
  const btn = e.target.closest('.ctl button');
  if (!btn) return;
  e.preventDefault();
  const el = btn.closest('.blk');
  const sibs = siblings(el);
  const i = sibs.indexOf(el);
  const act = btn.dataset.act;
  if (act === 'up' &amp;&amp; i &gt; 0) sibs[i - 1].before(el);
  else if (act === 'down' &amp;&amp; i &lt; sibs.length - 1) sibs[i + 1].after(el);
  else if (act === 'del') el.remove();
  else if (act === 'brk') el.classList.toggle('page-break');
});
</code></pre>
<p><code>siblings()</code> is where <code>dataset.sel</code> pays off: it filters the parent's children down to blocks <em>of the same kind</em>, so a job can never swap into the middle of a bullet list. <code>before()</code> and <code>after()</code> move the live node with no cloning or re-rendering, and the block's own controls travel with it.</p>
<p>There's one subtle bug to prevent. Clicking a button inside an editable region moves the text caret first, which can scroll the page or collapse a selection. Suppress it at <code>mousedown</code>, before the browser acts:</p>
<pre><code class="language-js">page.addEventListener('mousedown', e =&gt; {
  if (e.target.closest('.ctl, .add-btn')) e.preventDefault();
});
</code></pre>
<p>Forgetting this is the kind of thing you only notice as a vague feeling that clicking buttons "jumps." It's worth ruling out before it ships.</p>
<h2 id="heading-how-to-edit-links-inside-contenteditable">How to Edit Links Inside <code>contenteditable</code></h2>
<p>Inside an editable region, single-clicking a link places the caret instead of navigating. That's correct for text editing but leaves no way to change the URL itself. The <code>href</code> isn't text, it's an attribute.</p>
<p>Double-click is unclaimed real estate, so hang URL editing off it:</p>
<pre><code class="language-js">page.addEventListener('dblclick', e =&gt; {
  const a = e.target.closest('a');
  if (!a) return;
  e.preventDefault();
  const url = prompt('Link URL (leave empty to remove the link):', a.getAttribute('href'));
  if (url === null) return;
  if (!url.trim()) a.replaceWith(document.createTextNode(a.textContent));
  else a.setAttribute('href', url.trim());
});
</code></pre>
<p>Yes, <code>prompt()</code>. It's unfashionable, but consider what a custom modal would cost: markup, styles, focus management, and an escape handler, all for a dialog that asks one question. <code>prompt()</code> is native, keyboard-accessible, and can't break.</p>
<p>The empty-string branch is a nice touch: it unwraps the link entirely, replacing it with its own text, so "remove this link" doesn't require knowing any HTML.</p>
<p>Since none of this is discoverable, tell the user. A <code>title</code> attribute on every link ("Double-click to change this link") surfaces the affordance exactly where it's needed.</p>
<h2 id="heading-how-to-let-users-control-print-pagination">How to Let Users Control Print Pagination</h2>
<p>If the document's destination is a printed PDF, page breaks are content decisions, like "start my Skills section on page three," and the user should own them. CSS makes the break itself easy:</p>
<pre><code class="language-css">@media print {
  .page-break { break-before: page; page-break-before: always; }
  .job { break-inside: avoid; page-break-inside: avoid; }
}
</code></pre>
<p>The interesting part is the interface. The <code>⇟</code> button you saw in <code>makeBlock</code> just toggles the <code>page-break</code> class on a job or section. On screen, the class renders as a dashed accent line above the block, a visible seam showing where the printed page will end:</p>
<pre><code class="language-css">.page .page-break {
  border-top: 1.5px dashed var(--accent) !important;
  padding-top: 14px !important;
}

@media print {
  .page .page-break { border-top: none !important; padding-top: 0 !important; }
}
</code></pre>
<p>The dashed line exists only on screen. In print it vanishes and the actual break takes its place. The user toggles, glances at the seam, and prints. Nobody edits CSS to re-paginate a document, and just as importantly, nobody asks me to.</p>
<p>The same <code>@media print</code> block hides every piece of editing chrome (<code>.toolbar, .ctl, .add-btn { display: none !important; }</code>), so the printed output is indistinguishable from the original static page.</p>
<h2 id="heading-how-to-add-new-content-from-templates">How to Add New Content from Templates</h2>
<p>Editing and deleting only go so far. Eventually someone needs a new bullet, a new role, or a new certification. Each repeatable container gets a dashed "+ Add" button that builds a blank block from a template:</p>
<pre><code class="language-js">skillsSection.appendChild(newAddBtn('+ Add skill row', 'Adds a blank row. Type over the placeholder.', btn =&gt; {
  const row = document.createElement('div');
  row.className = 'skills-row';
  row.innerHTML =
    '&lt;span class="skill-label"&gt;Label&lt;/span&gt;' +
    '&lt;span class="skill-items"&gt;Skill one, skill two, skill three&lt;/span&gt;';
  skillsSection.insertBefore(row, btn);
  makeBlock(row, '.skills-row');
  selectText(row.querySelector('.skill-label'));
}));
</code></pre>
<p>Two details here do most of the work.</p>
<p>New blocks go through the same <code>makeBlock</code> as everything parsed at load. There is exactly one code path for "this is a block now", so added content is immediately movable and deletable. For roles it gets its own nested "+ Add bullet" button.</p>
<p>If you find yourself writing a second registration path for dynamic content, stop. You're about to fork behavior that must stay identical.</p>
<p>And <code>selectText</code> pre-selects the placeholder:</p>
<pre><code class="language-js">function selectText(node) {
  const range = document.createRange();
  range.selectNodeContents(node);
  const sel = getSelection();
  sel.removeAllRanges();
  sel.addRange(range);
}
</code></pre>
<p>Click "+ Add skill row" and the word <code>Label</code> is already highlighted, so typing replaces it. No clicking into the field, no manually deleting placeholder text, and no placeholders accidentally left in the printed document.</p>
<p>One caveat: select the <em>text node</em>, not the block. The block contains your <code>contenteditable="false"</code> control cluster, and a selection spanning it will delete your buttons along with the placeholder on the first keystroke.</p>
<h2 id="heading-why-nothing-persists">Why Nothing Persists</h2>
<p>Every edit lives in the DOM and dies on refresh. That sounds like the missing feature, but it's the design.</p>
<p>The workflow this page serves is: open, adjust, print to PDF, close. The PDF is the artifact. The page is a template you stamp from. Ephemerality gives you a free, bulletproof undo-everything (refresh), zero risk of a half-finished edit becoming the new baseline, and a canonical version that always matches source control.</p>
<p>The toolbar says it plainly: <em>"Nothing is saved. Refresh resets everything."</em> Stated upfront, it reads as a guarantee rather than a gotcha.</p>
<p>Persistence would also be the complexity cliff. The moment edits survive refresh you inherit serialization, versioning, merge conflicts with the source file, and "which copy is real?" Those are the exact problems this design exists to avoid.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>You now have a static HTML page that edits itself: <code>contenteditable</code> for text, one <code>makeBlock</code> function for structure, <code>:has()</code> for focused hover controls, a class toggle for print pagination, and templates with pre-selected placeholders for new content. Around a hundred lines of JavaScript, with no dependencies and no build.</p>
<p>Just as important is knowing when this approach stops being right. If edits must persist, if multiple people edit concurrently, or if the content needs validation and workflow, you've outgrown the DOM-as-state model. In those cases, reach for a real application and a database.</p>
<p>But for the wide middle ground of documents that one person adjusts and prints, such as résumés, invoices, certificates, and programmes, the browser already ships the editor. You just have to turn it on.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Reusable Date-Time Picker in React with shadcn/ui ]]>
                </title>
                <description>
                    <![CDATA[ A date and time picker is one of those components that looks small in a design file and turns into a real time sink once you start building it. You need a calendar, a time selector, a state that keeps ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-a-reusable-date-time-picker-in-react-with-shadcn-ui/</link>
                <guid isPermaLink="false">6a60f42cb1ecbbb606535a7b</guid>
                
                    <category>
                        <![CDATA[ shadcn ui ]]>
                    </category>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Vaibhav Gupta ]]>
                </dc:creator>
                <pubDate>Wed, 22 Jul 2026 16:47:40 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/7dcddc1c-2a9d-4af7-8f02-ffb76bea2c7b.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>A date and time picker is one of those components that looks small in a design file and turns into a real time sink once you start building it. You need a calendar, a time selector, a state that keeps both in sync, and usually a range mode and a translated version somewhere down the line, too.</p>
<p>This guide walks through ready-made picker patterns you can drop into a React project today: a combined date and time picker, a date range picker, and a time picker.</p>
<p>Every one of these is available as a <a href="https://shadcnspace.com/components/date-picker"><strong>Shadcn Date Picker</strong></a> component you can install with a single CLI command instead of building from scratch.</p>
<p>These components are built on both Radix and Base UI primitives, and the versions below use Base UI. They also support copy-prompt functionality, so you can paste them straight into v0, Lovable, or Bolt if that's part of your workflow.</p>
<h2 id="heading-table-of-contents"><strong>Table of Contents</strong></h2>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-what-youll-build">What You'll Build</a></p>
</li>
<li><p><a href="#heading-how-to-install-a-shadcn-date-time-picker">How to Install a Shadcn Date Time Picker</a></p>
</li>
<li><p><a href="#heading-how-to-build-a-date-and-time-picker">How to Build a Date and Time Picker</a></p>
</li>
<li><p><a href="#heading-how-to-build-a-date-range-picker">How to Build a Date Range Picker</a></p>
</li>
<li><p><a href="#heading-how-to-build-a-time-picker">How to Build a Time Picker</a></p>
</li>
<li><p><a href="#heading-live-preview-of-the-components">Live Preview of the components</a></p>
</li>
<li><p><a href="#heading-key-concepts-recap">Key Concepts Recap</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
<li><p><a href="#heading-resources">Resources</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To follow along, you should know:</p>
<ul>
<li><p>The basics of React, including <code>useState</code> and props</p>
</li>
<li><p>How to install components with the shadcn/ui CLI</p>
</li>
<li><p>Basic Tailwind CSS class names</p>
</li>
</ul>
<p>You also need a React project with shadcn/ui already set up. If you haven't done that yet, run the shadcn/ui CLI setup command in your project before continuing.</p>
<h2 id="heading-what-youll-build">What You'll Build</h2>
<ul>
<li><p>A <code>DateTimePicker</code> component that combines a calendar and time slots into one value.</p>
</li>
<li><p>A <code>TimePicker</code> variant that reuses the same time-slot logic without a calendar.</p>
</li>
<li><p>A <code>DateRangePicker</code> that lets a user pick a start and end date.</p>
</li>
</ul>
<h2 id="heading-how-to-install-a-shadcn-date-time-picker"><strong>How to Install a Shadcn Date Time Picker</strong></h2>
<p>All the components below install through the same CLI pattern. Pick the package manager you use:</p>
<p><strong>pnpm</strong></p>
<pre><code class="language-javascript">pnpm dlx shadcn@latest add @shadcn-space/date-picker-01
</code></pre>
<p><strong>npm</strong></p>
<pre><code class="language-javascript">npx shadcn@latest add @shadcn-space/date-picker-01
</code></pre>
<p><strong>yarn</strong></p>
<pre><code class="language-javascript">yarn dlx shadcn@latest add @shadcn-space/date-picker-01
</code></pre>
<p><strong>bun</strong></p>
<pre><code class="language-javascript">bunx --bun shadcn@latest add @shadcn-space/date-picker-01
</code></pre>
<p>Every other component below installs the same way: just swap the package name at the end of the command. If you haven't set up the CLI in your project yet, this <a href="https://shadcnspace.com/docs/getting-started/how-to-use-shadcn-cli"><strong>getting-started guide</strong></a> covers that first and shows how to integrate these components when you're working through an MCP-connected editor.</p>
<h2 id="heading-how-to-build-a-date-and-time-picker"><strong>How to Build a Date and Time Picker</strong></h2>
<p>This is the combined picker: a calendar popover for the date, plus start and end time fields, wrapped around a booking confirmation flow.</p>
<p><strong>Folder structure:</strong></p>
<pre><code class="language-javascript">components
└── shadcn-space
    └── date-picker
        └── date-picker-01.tsx
</code></pre>
<p><strong>Component code:</strong></p>
<pre><code class="language-javascript">"use client";
import { useState } from "react";
import { format } from "date-fns";
import { CalendarIcon, Clock, ChevronDown, Check } from "lucide-react";
import { cn } from "@/lib/utils";

import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover";

const DateAndTimePickerDemo = () =&gt; {
  const [open, setOpen] = useState(false);
  const [date, setDate] = useState&lt;Date | undefined&gt;(undefined);
  const [bookingStatus, setBookingStatus] = useState&lt;
    "idle" | "loading" | "success"
  &gt;("idle");

  const handleBooking = () =&gt; {
    setBookingStatus("loading");
    setTimeout(() =&gt; setBookingStatus("success"), 1500);
  };

  return (
    &lt;&gt;
      &lt;div className="grid gap-6"&gt;
        &lt;div className="grid gap-2"&gt;
          &lt;Label htmlFor="date" className="text-sm font-semibold"&gt;
            Select Date
          &lt;/Label&gt;
          &lt;Popover open={open} onOpenChange={setOpen}&gt;
            &lt;PopoverTrigger
              onPointerDown={() =&gt; setBookingStatus("idle")}
              render={
                &lt;Button
                  variant="outline"
                  id="date"
                  className={cn(
                    "w-full justify-start text-left font-normal h-10 transition-all hover:bg-muted/50 cursor-pointer",
                    !date &amp;&amp; "text-muted-foreground",
                  )}
                &gt;
                  &lt;CalendarIcon className="mr-2 h-4 w-4 opacity-70" /&gt;
                  {date ? format(date, "PPP") : &lt;span&gt;Select a date&lt;/span&gt;}
                  &lt;ChevronDown className="ml-auto h-4 w-4 opacity-50" /&gt;
                &lt;/Button&gt;
              }
            /&gt;
            &lt;PopoverContent
              className="w-auto p-0 border-muted-foreground/10 shadow-2xl"
              align="start"
            &gt;
              &lt;Calendar
                mode="single"
                selected={date}
                onSelect={(d) =&gt; {
                  setDate(d);
                  setOpen(false);
                }}
                className="rounded-md border-none"
              /&gt;
            &lt;/PopoverContent&gt;
          &lt;/Popover&gt;
        &lt;/div&gt;

        &lt;div className="grid grid-cols-2 gap-4"&gt;
          &lt;div className="grid gap-2"&gt;
            &lt;Label
              htmlFor="time-from"
              className="text-sm font-semibold text-muted-foreground flex items-center gap-1.5"
            &gt;
              &lt;Clock className="size-3.5" /&gt; Start Time
            &lt;/Label&gt;
            &lt;Input
              type="time"
              id="time-from"
              defaultValue="09:00"
              className="h-10 bg-background appearance-none transition-all focus:ring-2 focus:ring-primary/20"
            /&gt;
          &lt;/div&gt;
          &lt;div className="grid gap-2"&gt;
            &lt;Label
              htmlFor="time-to"
              className="text-sm font-semibold text-muted-foreground flex items-center gap-1.5"
            &gt;
              &lt;Clock className="size-3.5" /&gt; End Time
            &lt;/Label&gt;
            &lt;Input
              type="time"
              id="time-to"
              defaultValue="10:00"
              className="h-10 bg-background appearance-none transition-all focus:ring-2 focus:ring-primary/20"
            /&gt;
          &lt;/div&gt;
        &lt;/div&gt;

        &lt;Button
          className="w-full h-11 font-semibold transition-all group overflow-hidden relative cursor-pointer"
          onClick={handleBooking}
          disabled={!date || bookingStatus !== "idle"}
        &gt;
          {bookingStatus === "idle" &amp;&amp; (
            &lt;span className="flex items-center gap-2"&gt;Confirm Meet&lt;/span&gt;
          )}
          {bookingStatus === "loading" &amp;&amp; (
            &lt;div className="flex items-center gap-2"&gt;
              &lt;div className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" /&gt;
              Processing...
            &lt;/div&gt;
          )}
          {bookingStatus === "success" &amp;&amp; (
            &lt;span className="flex items-center gap-2 animate-in zoom-in-50 duration-300"&gt;
              &lt;Check className="h-4 w-4" /&gt;
              Meet Scheduled!
            &lt;/span&gt;
          )}
        &lt;/Button&gt;
      &lt;/div&gt;
    &lt;/&gt;
  );
};

export default DateAndTimePickerDemo;
</code></pre>
<h3 id="heading-how-this-component-works">How This Component Works</h3>
<p>This component combines three pieces of state into a simple booking flow:</p>
<ul>
<li><p>The selected date</p>
</li>
<li><p>Start and end times</p>
</li>
<li><p>The booking status (idle, loading, success)</p>
</li>
</ul>
<p>The date is stored using React state:</p>
<pre><code class="language-javascript">const [date, setDate] = useState&lt;Date | undefined&gt;(undefined);
</code></pre>
<p>When a user selects a day from the calendar, the <code>onSelect</code> callback updates the state and closes the popover:</p>
<pre><code class="language-javascript">onSelect={(d) =&gt; {
  setDate(d);
  setOpen(false);
}}
</code></pre>
<p>The calendar itself lives inside a <code>Popover</code>, which keeps the UI compact. Clicking the trigger button opens the calendar panel:</p>
<pre><code class="language-plaintext">&lt;Popover open={open} onOpenChange={setOpen}&gt;
</code></pre>
<p>The displayed date uses <code>date-fns</code> formatting:</p>
<pre><code class="language-plaintext">format(date, "PPP")
</code></pre>
<p>This converts a JavaScript <code>Date</code> object into a readable format such as:</p>
<pre><code class="language-plaintext">July 22, 2026
</code></pre>
<p>The time fields use native HTML inputs:</p>
<pre><code class="language-plaintext">&lt;Input type="time" /&gt;
</code></pre>
<p>Native time inputs provide built-in browser support, mobile pickers, keyboard accessibility, and locale-aware formatting without additional libraries.</p>
<p>The booking button demonstrates how UI state can change during an async action:</p>
<pre><code class="language-plaintext">"idle" → "loading" → "success"
</code></pre>
<p>In a real application, <code>handleBooking()</code> would typically call an API endpoint instead of using <code>setTimeout</code>.</p>
<p>This pattern works well for:</p>
<ul>
<li><p>Meeting schedulers</p>
</li>
<li><p>Appointment systems</p>
</li>
<li><p>Interview booking tools</p>
</li>
<li><p>Event registration forms</p>
</li>
<li><p>SaaS scheduling workflows</p>
</li>
</ul>
<h2 id="heading-how-to-build-a-date-range-picker"><strong>How to Build a Date Range Picker</strong></h2>
<p>For anything involving a stay, a rental, or a multi-day booking, you need a range instead of a single date. This component pairs a two-month calendar view with a formatted range label and a computed night count.</p>
<p><strong>Folder structure:</strong></p>
<pre><code class="language-javascript">components
└── shadcn-space
    └── date-picker
        └── date-picker-02.tsx
</code></pre>
<p>Install this Shadcn Date Range Picker with:</p>
<pre><code class="language-javascript">npx shadcn@latest add @shadcn-space/date-picker-02
</code></pre>
<p><strong>Component code:</strong></p>
<pre><code class="language-javascript">"use client";
import * as React from "react";
import { CalendarIcon, ChevronDown } from "lucide-react";
import { addDays, format } from "date-fns";
import { DateRange } from "react-day-picker";

import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover";
import { Label } from "@/components/ui/label";

const DateRangePickerDemo = () =&gt; {
  const [date, setDate] = React.useState&lt;DateRange | undefined&gt;({
    from: new Date(),
    to: addDays(new Date(), 7),
  });

  return (
    &lt;div className="grid gap-3 max-w-sm mx-auto"&gt;
      &lt;Label htmlFor="date-range" className="text-sm font-medium px-1"&gt;
        Select Travel Dates
      &lt;/Label&gt;
      &lt;div className={cn("grid gap-2")}&gt;
        &lt;Popover&gt;
          &lt;PopoverTrigger
            render={
              &lt;Button
                id="date-range"
                variant={"outline"}
                className={cn(
                  "w-full justify-start text-left font-normal h-11 transition-all hover:bg-muted/50 focus:ring-2 focus:ring-primary/20 cursor-pointer",
                  !date &amp;&amp; "text-muted-foreground",
                )}
              &gt;
                &lt;CalendarIcon className="mr-2 h-4 w-4 opacity-70" /&gt;
                {date?.from ? (
                  date.to ? (
                    &lt;&gt;
                      {format(date.from, "LLL dd, y")} -{" "}
                      {format(date.to, "LLL dd, y")}
                    &lt;/&gt;
                  ) : (
                    format(date.from, "LLL dd, y")
                  )
                ) : (
                  &lt;span&gt;Pick a range&lt;/span&gt;
                )}
                &lt;ChevronDown className="ml-auto h-4 w-4 opacity-50" /&gt;
              &lt;/Button&gt;
            }
          /&gt;
          &lt;PopoverContent
            className="w-auto p-0 border-muted/20 shadow-xl"
            align="start"
          &gt;
            &lt;Calendar
              mode="range"
              defaultMonth={date?.from}
              selected={date}
              onSelect={setDate}
              numberOfMonths={2}
              className="p-3"
            /&gt;
          &lt;/PopoverContent&gt;
        &lt;/Popover&gt;
      &lt;/div&gt;
      &lt;p className="text-xs text-muted-foreground px-1"&gt;
        {date?.from &amp;&amp; date?.to
          ? `Stay duration: ${Math.round((date.to.getTime() - date.from.getTime()) / (1000 * 60 * 60 * 24))} nights`
          : "Please select a valid date range."}
      &lt;/p&gt;
    &lt;/div&gt;
  );
};

export default DateRangePickerDemo;
</code></pre>
<h3 id="heading-how-the-date-range-picker-works">How the Date Range Picker Works</h3>
<p>This version switches the Calendar component from single-date mode to range mode:</p>
<pre><code class="language-javascript">mode="range"
</code></pre>
<p>Instead of storing one <code>Date</code>, the component stores a <code>DateRange</code> object:</p>
<pre><code class="language-javascript">{
  from: Date,
  to: Date
}
</code></pre>
<p>This makes it easy to work with booking systems, hotel stays, travel forms, and rental applications.</p>
<p>The component displays two calendar months:</p>
<pre><code class="language-javascript">numberOfMonths={2}
</code></pre>
<p>Showing two months reduces navigation and improves the selection experience for longer stays.</p>
<p>The stay duration is calculated directly from the selected dates:</p>
<pre><code class="language-javascript">(date.to.getTime() - date.from.getTime())
</code></pre>
<p>This avoids additional libraries and keeps the logic simple.</p>
<p>Compared to the first example:</p>
<ul>
<li><p>Uses <code>DateRange</code> instead of a single <code>Date</code></p>
</li>
<li><p>Uses <code>mode="range"</code></p>
</li>
<li><p>Displays two months</p>
</li>
<li><p>Adds derived data like total nights</p>
</li>
</ul>
<h2 id="heading-how-to-build-a-time-picker"><strong>How to Build a Time Picker</strong></h2>
<p>Not every form needs a calendar. This one wraps the native in an InputGroup with a clock icon that triggers the browser's own time picker UI on click.</p>
<p><strong>Folder structure:</strong></p>
<pre><code class="language-javascript">components
└── shadcn-space
    └── date-picker
        └── date-picker-03.tsx
</code></pre>
<p>Install this Shadcn Time Picker with:</p>
<pre><code class="language-javascript">npx shadcn@latest add @shadcn-space/date-picker-03
</code></pre>
<p><strong>Component code:</strong></p>
<pre><code class="language-javascript">"use client";
import { useRef } from "react";
import { Label } from "@/components/ui/label";
import {
  InputGroup,
  InputGroupAddon,
  InputGroupInput,
} from "@/components/ui/input-group";
import { Clock8Icon } from "lucide-react";

const TimePickerWithIconDemo = () =&gt; {
  const inputRef = useRef&lt;HTMLInputElement&gt;(null);

  const handleShowPicker = () =&gt; {
    if (inputRef.current &amp;&amp; "showPicker" in inputRef.current) {
      try {
        inputRef.current.showPicker();
      } catch (error) {
        console.error("Failed to open native picker:", error);
      }
    }
  };

  return (
    &lt;div className="flex w-full max-w-xs flex-col gap-2"&gt;
      &lt;Label htmlFor="time-picker"&gt;Select Slot&lt;/Label&gt;
      &lt;InputGroup&gt;
        &lt;InputGroupAddon
          align="inline-start"
          className="cursor-pointer hover:text-foreground transition-colors"
          onClick={handleShowPicker}
          title="Open time picker"
        &gt;
          &lt;Clock8Icon className="size-4" /&gt;
        &lt;/InputGroupAddon&gt;
        &lt;InputGroupInput
          ref={inputRef}
          type="time"
          id="time-picker"
          step="1"
          defaultValue="08:30:00"
          className="appearance-none [&amp;::-webkit-calendar-picker-indicator]:hidden [&amp;::-webkit-calendar-picker-indicator]:appearance-none"
        /&gt;
      &lt;/InputGroup&gt;
    &lt;/div&gt;
  );
};
export default TimePickerWithIconDemo;
</code></pre>
<h3 id="heading-how-the-time-picker-works">How the Time Picker Works</h3>
<p>This component uses the browser's native time picker instead of building a custom dropdown.</p>
<p>The input is referenced with <code>useRef</code>:</p>
<pre><code class="language-javascript">const inputRef = useRef&lt;HTMLInputElement&gt;(null);
</code></pre>
<p>This allows the icon button to access the input element directly.</p>
<p>When users click the clock icon, the browser's picker opens programmatically:</p>
<pre><code class="language-javascript">inputRef.current.showPicker();
</code></pre>
<p>The <code>showPicker()</code> method is a modern browser API that opens the same interface users would see if they clicked the input manually.</p>
<p>The component wraps the input inside an <code>InputGroup</code>:</p>
<pre><code class="language-plaintext">&lt;InputGroup&gt;
</code></pre>
<p>This creates a cleaner layout where the icon behaves as part of the field rather than a separate button.</p>
<p>The browser's default picker icon is hidden:</p>
<pre><code class="language-plaintext">[&amp;::-webkit-calendar-picker-indicator]:hidden
</code></pre>
<p>This prevents duplicate icons and gives full control over the UI.</p>
<p>The benefit of this approach is that it keeps:</p>
<ul>
<li><p>Native accessibility</p>
</li>
<li><p>Mobile keyboard support</p>
</li>
<li><p>Locale-aware formatting</p>
</li>
<li><p>Better browser compatibility</p>
</li>
</ul>
<p>Instead of rebuilding time selection from scratch, the component improves the native experience with custom styling.</p>
<h2 id="heading-live-preview-of-the-components"><strong>Live Preview of the Components</strong></h2>
<img src="https://cdn.hashnode.com/uploads/covers/68b53a3d851476bd2ce87f12/f815455d-9f2d-4ad1-ac9c-155c37aae094.gif" alt="f815455d-9f2d-4ad1-ac9c-155c37aae094" style="display:block;margin:0 auto" width="1152" height="648" loading="lazy">

<h2 id="heading-key-concepts-recap"><strong>Key Concepts Recap</strong></h2>
<ul>
<li><p><strong>Pick the pattern that matches the data you're collecting</strong>: A single date and time for bookings, a range for stays, a bare time field for slots.</p>
</li>
<li><p><strong>Every component installs the same way</strong>: One CLI command per component: <code>pnpm</code>, <code>npm</code>, <code>yarn</code>, and <code>bun</code>. All are supported. Adding a new picker to a project takes one line, not a manual build.</p>
</li>
<li><p><strong>The date and time picker separates date state from time state</strong>, then combines them at the confirmation step. This keeps the calendar and the time inputs from getting out of sync.</p>
</li>
<li><p><strong>The range picker leans on the Calendar's built-in</strong> <code>mode="range"</code>: The two-month view and the night count come from the selection state, not custom range math.</p>
</li>
<li><p><strong>The time picker wraps the native</strong> <code>&lt;input type="time"&gt;</code> <strong>instead of replacing it</strong>: This keeps native accessibility and mobile keyboard behavior intact.</p>
</li>
</ul>
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>You now have working picker patterns for React: a date and time picker for bookings, a date range picker for stays, and a plain time picker for slots.</p>
<p>Each one installs with a single CLI command, so you can pick whichever one matches the form you're building and have it running in minutes.</p>
<h2 id="heading-resources"><strong>Resources</strong></h2>
<ul>
<li><p><a href="https://ui.shadcn.com/docs"><strong>shadcn/ui documentation</strong></a>: For the official Popover, Calendar, and Button component APIs</p>
</li>
<li><p><a href="https://date-fns.org/docs/Getting-Started"><strong>date-fns documentation</strong></a>: For the date helper functions and locale files used in this tutorial</p>
</li>
<li><p><a href="https://daypicker.dev/"><strong>react-day-picker documentation</strong></a>: For the range and multi-date selection modes that power the shadcn/ui Calendar</p>
</li>
<li><p><a href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions"><strong>MDN: ARIA live regions</strong></a>: For more on how <code>role="alert"</code> works</p>
</li>
<li><p><a href="https://shadcnspace.com/components/date-picker"><strong>Shadcn Date Time Picker components</strong></a>: If you want more prebuilt date and time picker variants to compare against.</p>
</li>
<li><p><a href="https://shadcnspace.com/components"><strong>Full Shadcn components library</strong></a>: For other shadcn/ui-compatible components</p>
</li>
<li><p><a href="https://wrappixel.com/blog/shadcn-date-picker"><strong>Guide to free Shadcn date and time pickers</strong></a></p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Use Apple’s Foundation Models in a Web App with a macOS Companion ]]>
                </title>
                <description>
                    <![CDATA[ Not every AI feature needs a cloud model, with its per-token bills, network round-trips, and private data leaving your machine. If you're on a modern Mac, a capable language model is already on your d ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-use-apple-s-foundation-models-in-a-web-app-with-a-macos-companion/</link>
                <guid isPermaLink="false">6a5e92afe12aa31dae6e8a79</guid>
                
                    <category>
                        <![CDATA[ software development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ macOS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Swift ]]>
                    </category>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Balogun Wahab ]]>
                </dc:creator>
                <pubDate>Mon, 20 Jul 2026 21:27:11 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/7f0e2343-7394-46b5-a4c8-3ef0fecfa57a.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Not every AI feature needs a cloud model, with its per-token bills, network round-trips, and private data leaving your machine. If you're on a modern Mac, a capable language model is already on your disk.</p>
<p><strong>Foundation Models</strong> is Apple's Swift framework for working with large language models. It's the on-device model behind Apple Intelligence, Apple's Private Cloud Compute, or another provider's server model.</p>
<p>This tutorial targets the on-device model: you send it a prompt and it runs entirely on the Mac's own hardware locally, free-per-call, and offline-friendly.</p>
<p>Paired with Apple Vision for reading images on device, that's enough to build real AI features like summaries, classification, and structured extraction without the data ever leaving your machine.</p>
<h2 id="heading-table-of-contents">Table Of Contents</h2>
<ul>
<li><p><a href="#heading-what-you-will-build">What You Will Build</a></p>
</li>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-why-a-macos-companion-app">Why a macOS Companion App?</a></p>
</li>
<li><p><a href="#heading-foundation-models-cant-read-images-directly">Foundation Models Can't Read Images Directly</a></p>
</li>
<li><p><a href="#heading-project-structure">Project Structure</a></p>
</li>
<li><p><a href="#heading-build-the-react-app">Build the React App</a></p>
<ul>
<li><p><a href="#heading-check-companion-health">Check Companion Health</a></p>
</li>
<li><p><a href="#heading-convert-the-image-to-base64">Convert the Image to Base64</a></p>
</li>
<li><p><a href="#heading-analyze-immediately-after-upload">Analyze Immediately After Upload</a></p>
</li>
<li><p><a href="#heading-send-the-image-to-the-companion">Send the Image to the Companion</a></p>
</li>
<li><p><a href="#heading-render-the-json-output">Render the JSON Output</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-build-the-macos-companion-app">Build the macOS Companion App</a></p>
</li>
<li><p><a href="#heading-check-foundation-models-availability">Check Foundation Models Availability</a></p>
</li>
<li><p><a href="#heading-extract-text-with-apple-vision">Extract Text with Apple Vision</a></p>
</li>
<li><p><a href="#heading-ask-foundation-models-to-explain-the-vision-output">Ask Foundation Models to Explain the Vision Output</a></p>
</li>
<li><p><a href="#heading-return-json-to-the-browser">Return JSON to the Browser</a></p>
</li>
<li><p><a href="#heading-run-the-app">Run the App</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
<li><p><a href="#heading-resources">Resources</a></p>
</li>
</ul>
<h2 id="heading-what-you-will-build">What You Will Build</h2>
<p>You'll build <strong>Vision Bridge</strong>, a web app that sends an image to a local macOS companion. The companion reads the image with Apple Vision, reasons about it with Foundation Models, and returns structured JSON to the browser: private, on-device AI behind a plain web interface.</p>
<p>You can find the complete source code in this GitHub repository: <a href="http://github.com/03balogun/vision-bridge">github.com/03balogun/vision-bridge</a>.</p>
<p>The goal isn't to build a giant product but rather to understand the architecture behind how this works.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5db93b3da2342e8354088115/6d18db01-e921-4291-bb2e-26be2c02b304.png" alt="Screenshot of the Vision Bridge app, with image upload on the left and JSON output on the right" style="display:block;margin:0 auto" width="3024" height="1714" loading="lazy">

<p>Vision Bridge has two parts:</p>
<ul>
<li><p>A React app with a split-screen interface.</p>
</li>
<li><p>A macOS companion app that exposes a local API.</p>
</li>
</ul>
<p>The React app has:</p>
<ul>
<li><p>An image upload area</p>
</li>
<li><p>An image preview</p>
</li>
<li><p>Automatic analysis after upload</p>
</li>
<li><p>A JSON output viewer</p>
</li>
<li><p>A companion health status indicator</p>
</li>
</ul>
<p>The macOS companion app has:</p>
<ul>
<li><p><code>GET /v1/health</code></p>
</li>
<li><p><code>POST /v1/analyze-image</code></p>
</li>
<li><p>Apple Vision OCR</p>
</li>
<li><p>Foundation Models availability checks</p>
</li>
<li><p>Foundation Models reasoning over Vision output</p>
</li>
</ul>
<p>The final response looks like this:</p>
<pre><code class="language-json">{
  "support": {
    "visionAvailable": true,
    "foundationModelAvailable": true,
    "foundationModelStatus": "available"
  },
  "image": {
    "filename": "screenshot.png",
    "contentType": "image/png",
    "byteCount": 1048576,
    "width": 1440,
    "height": 900
  },
  "vision": {
    "detectedText": [
      {
        "text": "Build failed",
        "confidence": 0.96,
        "boundingBox": {
          "x": 0.12,
          "y": 0.31,
          "width": 0.45,
          "height": 0.08
        }
      }
    ]
  },
  "model": {
    "summary": "The image appears to show a software build failure.",
    "description": "A developer tool window is showing an error state with diagnostic text.",
    "suggestedTags": ["screenshot", "developer-tool", "error"],
    "possibleUses": [
      "Generate alt text",
      "Summarize screenshots",
      "Extract document data"
    ]
  }
}
</code></pre>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To follow along, you need:</p>
<ul>
<li><p>macOS 26 or newer</p>
</li>
<li><p>Xcode with the macOS 26 SDK</p>
</li>
<li><p>Node.js 20 or newer</p>
</li>
<li><p>Basic React knowledge</p>
</li>
<li><p>Basic Swift knowledge</p>
</li>
<li><p>A Mac that supports Apple Intelligence</p>
</li>
</ul>
<p>Foundation Models availability depends on the Mac, the OS version, and Apple Intelligence settings. The companion checks this at runtime, which we'll cover below.</p>
<h2 id="heading-why-a-macos-companion-app">Why a macOS Companion App?</h2>
<p>You can't write this in a regular React app:</p>
<pre><code class="language-ts">import FoundationModels from "apple-frameworks";
</code></pre>
<p>That API doesn't exist in the browser. A native macOS app, however, can use any Apple framework, so the companion acts as a local bridge. The same pattern works for any native capability the web platform doesn't expose.</p>
<h2 id="heading-foundation-models-cant-read-images-directly">Foundation Models Can't Read Images Directly</h2>
<p>The public Foundation Models framework is a language model interface. It doesn't currently expose direct image input the way a multimodal cloud model might, so this tutorial never sends the image to the model. Instead, the companion feeds the Vision OCR observations and image metadata into the prompt. The model reasons over structured text, never the original pixels.</p>
<p>That split plays to each framework's strength: Vision is excellent at pulling machine-readable information out of images, and Foundation Models turns that information into summaries, labels, explanations, and structured output.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5db93b3da2342e8354088115/a5c11ad4-dcac-4690-bc6d-08b27fd6fed8.png" alt="Vision Bridge architecture: the browser sends the image over localhost to the Swift companion, which runs Apple Vision OCR, feeds the observations to Foundation Models, and returns structured JSON" style="display:block;margin:0 auto" width="2492" height="1572" loading="lazy">

<p>The above diagram shows the round trip that the rest of this tutorial builds. The browser sends the uploaded image as base64 JSON over localhost to the Swift companion. Inside the companion, Apple Vision runs OCR on the image and produces text observations: the recognized strings, their confidence scores, and their bounding boxes.</p>
<p>Those observations, not the image itself, are formatted into a prompt for Foundation Models, which generates a summary, description, and tags. The companion then bundles the Vision output and the model output into one JSON response and returns it to the browser.</p>
<h2 id="heading-project-structure">Project Structure</h2>
<p>Create a project with this structure:</p>
<pre><code class="language-text">vision-bridge/
  apps/
    web/
      src/
        main.tsx
        styles.css
      package.json
      vite.config.ts
    macos-companion/
      Package.swift
      Sources/
        VisionBridgeCompanion/
          main.swift
  package.json
  README.md
</code></pre>
<p>The root <code>package.json</code> gives us a few convenient commands:</p>
<pre><code class="language-json">{
  "scripts": {
    "dev": "npm --workspace apps/web run dev",
    "build": "npm --workspace apps/web run build",
    "companion": "swift run --package-path apps/macos-companion VisionBridgeCompanion"
  },
  "workspaces": ["apps/web"]
}
</code></pre>
<h2 id="heading-build-the-react-app">Build the React App</h2>
<p>The web app is intentionally simple. It has one job: let the user pick an image and show the JSON returned by the companion.</p>
<p>The web app uses Vite, React, Lucide icons, and a JSON viewer:</p>
<pre><code class="language-json">{
  "dependencies": {
    "@vitejs/plugin-react": "^6.0.3",
    "lucide-react": "^0.468.0",
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "react-json-view-lite": "^2.5.0",
    "vite": "^8.1.3"
  }
}
</code></pre>
<p>After defining the dependencies, install them:</p>
<pre><code class="language-plaintext">npm install
</code></pre>
<p>The API base URL points to the local companion:</p>
<pre><code class="language-ts">const API_BASE_URL = "http://127.0.0.1:43119";
</code></pre>
<h3 id="heading-check-companion-health">Check Companion Health</h3>
<p>The web app pings the companion so the UI can show whether the native bridge is online:</p>
<pre><code class="language-ts">async function checkHealth() {
  setHealthError(null);

  try {
    const response = await fetch(`${API_BASE_URL}/v1/health`);
    if (!response.ok) {
      throw new Error(`Health check failed with ${response.status}`);
    }

    const payload = await response.json();
    setHealth(payload);
  } catch (error) {
    setHealth(null);
    setHealthError(error instanceof Error ? error.message : "Companion unavailable");
  }
}
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/5db93b3da2342e8354088115/dc3c37eb-1d9c-4b44-82db-f38adada4f19.png" alt="Screenshot of the companion online status pill" style="display:block;margin:0 auto" width="732" height="212" loading="lazy">

<h3 id="heading-convert-the-image-to-base64">Convert the Image to Base64</h3>
<p>When the user selects a file, the app converts it to base64 so it can be sent as JSON:</p>
<pre><code class="language-ts">function readFileAsBase64(file: File) {
  return new Promise&lt;string&gt;((resolve, reject) =&gt; {
    const reader = new FileReader();
    reader.onload = () =&gt; {
      const result = String(reader.result);
      resolve(result.includes(",") ? result.split(",")[1] : result);
    };
    reader.onerror = () =&gt; reject(reader.error);
    reader.readAsDataURL(file);
  });
}
</code></pre>
<p>This isn't the only way to upload files. You could also use <code>multipart/form-data</code>, but JSON keeps the demo easy to inspect.</p>
<h3 id="heading-analyze-immediately-after-upload">Analyze Immediately After Upload</h3>
<p>The app starts analysis as soon as an image is uploaded:</p>
<pre><code class="language-ts">async function handleFile(file: File) {
  if (!file.type.startsWith("image/")) {
    setError("Choose a PNG, JPEG, HEIC, or another browser-readable image.");
    return;
  }

  const base64 = await readFileAsBase64(file);
  const nextImage = {
    file,
    previewUrl: URL.createObjectURL(file),
    base64,
  };

  setSelectedImage(nextImage);
  setAnalysis(null);
  setError(null);
  setCopied(false);

  analyzeImage(nextImage);
}
</code></pre>
<p><code>handleFile</code> does the preparation work for every new image. It rejects anything that isn't a browser-readable image, converts the file to base64, and builds a single object holding everything the rest of the flow needs: the original <code>File</code> (for its name and MIME type), an object URL for the preview, and the base64 payload for the API call.</p>
<p>It then clears out the previous run the old analysis, any error message, and the "copied" indicator so the UI never shows results from the last image next to a new one. Finally, it kicks off <code>analyzeImage(nextImage)</code> immediately.</p>
<p>Note that it passes the fresh object directly instead of relying on the <code>selectedImage</code> state: React state updates don't apply until the next render, so reading the state here would still give you the <em>previous</em> image.</p>
<p>The <code>Analyze</code> button still exists in the UI, but it works as a manual rerun button.</p>
<h3 id="heading-send-the-image-to-the-companion">Send the Image to the Companion</h3>
<p>Here's the core request:</p>
<pre><code class="language-ts">const analysisRequestId = useRef(0);

async function analyzeImage(image = selectedImage) {
  if (!image) {
    setError("Choose an image first.");
    return;
  }

  const requestId = analysisRequestId.current + 1;
  analysisRequestId.current = requestId;

  setRequestState("loading");
  setError(null);
  setCopied(false);

  try {
    const response = await fetch(`${API_BASE_URL}/v1/analyze-image`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        filename: image.file.name,
        mimeType: image.file.type || "application/octet-stream",
        base64: image.base64,
      }),
    });

    const payload = await response.json();

    if (requestId !== analysisRequestId.current) {
      return;
    }

    if (!response.ok) {
      throw new Error(payload.error?.message ?? `Analysis failed with ${response.status}`);
    }

    setAnalysis(payload);
    setRequestState("success");
  } catch (error) {
    if (requestId !== analysisRequestId.current) {
      return;
    }

    setRequestState("error");
    setError(error instanceof Error ? error.message : "Could not analyze image");
  }
}
</code></pre>
<p>This function is the entire client side of the bridge. It flips <code>requestState</code> to <code>loading</code> (which drives the spinner and disables the button), then sends a <code>POST</code> to <code>/v1/analyze-image</code> with a JSON body containing three fields: the filename, the MIME type, and the base64 image data. That body maps one-to-one onto the <code>AnalyzeImageRequest</code> struct the Swift companion decodes later.</p>
<p>Notice that the response is parsed as JSON <em>before</em> checking <code>response.ok</code>. That's deliberate: when the companion rejects a request (bad base64, oversized image), it still returns a JSON body with an <code>error.message</code> field, so the UI can show the companion's own explanation instead of a generic status code. On success, the payload goes straight into state, and the JSON viewer re-renders with the result.</p>
<p>The <code>requestId</code> bookkeeping guards against stale responses. If a user uploads a second image while the first is still analyzing, whichever request finishes <em>last</em> would win, and OCR plus model generation takes long enough that responses can genuinely arrive out of order. So every call increments a counter stored in a ref and remembers its own ID.</p>
<p>After the <code>await</code>, it checks whether it's still the newest request; if a newer upload started in the meantime, the older response is silently discarded instead of overwriting the latest image's result. The same check runs in the <code>catch</code> block, so an old failure can't clobber a newer success either. If you also want to cancel the in-flight HTTP request rather than just ignore its result, an <code>AbortController</code> is the natural next step.</p>
<h3 id="heading-render-the-json-output">Render the JSON Output</h3>
<p>The output pane uses <code>react-json-view-lite</code>:</p>
<pre><code class="language-tsx">&lt;JsonView
  data={jsonData}
  shouldExpandNode={allExpanded}
  style={jsonViewTheme}
/&gt;
</code></pre>
<h2 id="heading-build-the-macos-companion-app">Build the macOS Companion App</h2>
<p>The companion is a Swift command-line app. It exposes a small local HTTP API.</p>
<p>If you come from the web side, the mapping is simple: Swift Package Manager is Swift's npm, <code>Package.swift</code> is its <code>package.json</code>, and <code>swift run</code> is its <code>npm start</code>. It ships with Xcode, so there's nothing extra to install.</p>
<p>The <code>Package.swift</code> file looks like this:</p>
<pre><code class="language-swift">// swift-tools-version: 6.0

import PackageDescription

let package = Package(
    name: "VisionBridgeCompanion",
    platforms: [
        .macOS("26.0")
    ],
    products: [
        .executable(
            name: "VisionBridgeCompanion",
            targets: ["VisionBridgeCompanion"]
        )
    ],
    targets: [
        .executableTarget(
            name: "VisionBridgeCompanion"
        )
    ]
)
</code></pre>
<p>The companion imports the Apple frameworks it needs:</p>
<pre><code class="language-swift">import Foundation
import FoundationModels
import ImageIO
import Network
import Vision
</code></pre>
<p>It listens on <code>127.0.0.1:43119</code>:</p>
<pre><code class="language-swift">private let defaultPort: UInt16 = 43119
</code></pre>
<p>The app exposes two routes:</p>
<pre><code class="language-swift">switch (request.method, request.path) {
case ("GET", "/v1/health"):
    let health = HealthResponse(support: ModelSupport.current)
    return try json(health)

case ("POST", "/v1/analyze-image"):
    let payload = try JSONDecoder().decode(AnalyzeImageRequest.self, from: request.body)
    let response = try await service.analyze(payload)
    return try json(response)

default:
    return try json(
        ErrorResponse(error: APIErrorPayload(message: "Route not found")),
        status: .notFound
    )
}
</code></pre>
<p>This <code>switch</code> is the companion's entire routing layer — no web framework, just pattern matching on the method and path.</p>
<p>The two routes split the work cleanly:</p>
<ul>
<li><p><code>GET /v1/health</code> is the cheap, read-only route. It runs no analysis, it just reports whether Vision and Foundation Models are usable on this Mac via <code>ModelSupport.current</code> (covered in the next section). The React app calls it on load to render the online/offline status pill, so the user knows the bridge is up before they upload anything.</p>
</li>
<li><p><code>POST /v1/analyze-image</code> is where the real work happens. It decodes the request body into an <code>AnalyzeImageRequest</code> (with the same <code>filename</code>, <code>mimeType</code>, and <code>base64</code> fields the browser sent) and hands it to the analysis service. This validates the image, runs Vision OCR, prompts Foundation Models, and returns the combined result. The <code>try await</code> matters here: analysis is asynchronous, and the route simply waits for it before serializing the response.</p>
</li>
</ul>
<p>Anything else falls through to a JSON 404, so even unknown routes respond in the same format the browser already knows how to parse.</p>
<p>Errors work the same way: thrown errors are caught in one place and converted into JSON error responses with an appropriate status code, which is exactly what the web app's <code>payload.error?.message</code> check reads.</p>
<p>One practical detail: because the browser calls the companion from a different origin (the Vite dev server), every response also carries CORS headers, and the router answers preflight <code>OPTIONS</code> requests with an empty <code>204</code>. Without that, the browser would block the <code>fetch</code> before it ever reached these routes.</p>
<h2 id="heading-check-foundation-models-availability">Check Foundation Models Availability</h2>
<p>The companion shouldn't assume that the model is available. Check it first:</p>
<pre><code class="language-swift">private struct ModelSupport: Encodable {
    let visionAvailable: Bool
    let foundationModelAvailable: Bool
    let foundationModelStatus: String

    static var current: ModelSupport {
        let model = SystemLanguageModel.default

        switch model.availability {
        case .available:
            return ModelSupport(
                visionAvailable: true,
                foundationModelAvailable: true,
                foundationModelStatus: "available"
            )

        case .unavailable(let reason):
            return ModelSupport(
                visionAvailable: true,
                foundationModelAvailable: false,
                foundationModelStatus: "unavailable.\(reason.description)"
            )

        @unknown default:
            return ModelSupport(
                visionAvailable: true,
                foundationModelAvailable: false,
                foundationModelStatus: "unavailable.unknown"
            )
        }
    }
}
</code></pre>
<p>A user might have an unsupported Mac, Apple Intelligence might be disabled, or the model might not be ready yet. The response tells the browser which case it's dealing with.</p>
<h2 id="heading-extract-text-with-apple-vision">Extract Text with Apple Vision</h2>
<p>The companion decodes the base64 image, checks its metadata, then runs Vision OCR.</p>
<p>Here's the text recognition flow:</p>
<pre><code class="language-swift">private func recognizeText(in imageData: Data) async throws -&gt; [DetectedText] {
    var request = RecognizeTextRequest()
    request.recognitionLevel = .accurate
    request.automaticallyDetectsLanguage = true
    request.usesLanguageCorrection = true

    let observations = try await request.perform(on: imageData)

    var detectedText: [DetectedText] = []

    for observation in observations {
        guard let candidate = observation.topCandidates(1).first else {
            continue
        }

        let bounds = NormalizedBox.from(points: [
            observation.topLeft,
            observation.topRight,
            observation.bottomRight,
            observation.bottomLeft
        ])

        detectedText.append(DetectedText(
            text: candidate.string,
            confidence: Double(candidate.confidence),
            boundingBox: bounds
        ))
    }

    return detectedText
}
</code></pre>
<p>Vision gives us structured observations:</p>
<ul>
<li><p>recognized text</p>
</li>
<li><p>confidence scores</p>
</li>
<li><p>normalized bounding boxes</p>
</li>
</ul>
<p>Those observations become the model’s context.</p>
<h2 id="heading-ask-foundation-models-to-explain-the-vision-output">Ask Foundation Models to Explain the Vision Output</h2>
<p>Now the companion creates a prompt from the image metadata and OCR results.</p>
<p>Notice the instruction:</p>
<pre><code class="language-text">You cannot see the original image. Use only the metadata and OCR observations below.
</code></pre>
<p>That keeps the model honest. It shouldn't pretend to see pixels it never received.</p>
<p>Here's the prompt shape:</p>
<pre><code class="language-swift">let textPreview = detectedText
    .prefix(30)
    .map { "- \($0.text) (confidence: \(String(format: "%.2f", $0.confidence)))" }
    .joined(separator: "\n")

let prompt = """
You are summarizing Apple Vision OCR output for a developer tool named Vision Bridge.
You cannot see the original image. Use only the metadata and OCR observations below.

Image:
- filename: \(image.filename)
- content type: \(image.contentType)
- size: \(image.width ?? 0)x\(image.height ?? 0)

OCR observations:
\(textPreview.isEmpty ? "- No text detected." : textPreview)

Return a compact JSON object with these exact keys:
summary: one sentence
description: one short paragraph
suggestedTags: 3 to 6 short tags
possibleUses: 3 to 5 practical use cases for this kind of image analysis
"""
</code></pre>
<p>Then call the model:</p>
<pre><code class="language-swift">let session = LanguageModelSession(
    model: .default,
    instructions: "Return valid JSON only. Do not include Markdown fences."
)

let response = try await session.respond(to: prompt)
let raw = response.content.trimmingCharacters(in: .whitespacesAndNewlines)
</code></pre>
<p>Even when you ask for JSON, always validate the output. Models can still return Markdown fences or malformed text. The sample app strips simple Markdown code fences and falls back to a raw response if parsing fails.</p>
<h2 id="heading-return-json-to-the-browser">Return JSON to the Browser</h2>
<p>The companion combines the support state, image metadata, Vision results, and model output:</p>
<pre><code class="language-swift">return AnalyzeImageResponse(
    support: support,
    image: metadata,
    vision: VisionPayload(detectedText: detectedText),
    model: modelInsight
)
</code></pre>
<p>The browser doesn't need to know how Vision or Foundation Models work. It just receives JSON. The native app owns the native capabilities, while the web app owns the interface.</p>
<p>It's worth pausing on what each of the four blocks actually gives you, because they're not all the same kind of data:</p>
<ul>
<li><p><code>support</code> tells you what was possible on this Mac. If <code>foundationModelAvailable</code> is <code>false</code>, the <code>model</code> block still exists but contains a fallback message rather than real analysis, and the <code>foundationModelStatus</code> string (for example, <code>unavailable.appleIntelligenceNotEnabled</code>) tells the UI <em>why</em>, so it can explain rather than silently degrade.</p>
</li>
<li><p><code>image</code> echoes back the file's metadata plus the measured pixel dimensions. It's useful as a sanity check, and you need the width and height to do anything spatial with the Vision results.</p>
</li>
<li><p><code>vision</code> is the ground truth. Each entry in <code>detectedText</code> is a string Vision actually found, with a confidence score between 0 and 1 and a normalized bounding box: coordinates expressed as fractions of the image size, so <code>x: 0.12, width: 0.45</code> means "starts 12% from the left and spans 45% of the width." Because the boxes are normalized, you can draw highlight overlays on the preview at any display size by multiplying by the rendered dimensions. Low-confidence entries are worth filtering or flagging before you trust them.</p>
</li>
<li><p><code>model</code> is interpretation, not observation. The <code>summary</code>, <code>description</code>, <code>suggestedTags</code>, and <code>possibleUses</code> fields are generated by the language model from the OCR text. This is useful as alt text, captions, or tag suggestions, but they inherit whatever the OCR missed and should be treated as a draft, not a fact. When the model's output can't be parsed as JSON, <code>rawResponse</code> carries the unparsed text so nothing is lost.</p>
</li>
</ul>
<p>For a screenshot of a failed build, the model block might come back like this:</p>
<pre><code class="language-json">{
  "model": {
    "summary": "The image appears to show a software build failure.",
    "description": "A developer tool window is showing an error state with diagnostic text.",
    "suggestedTags": ["screenshot", "developer-tool", "error"],
    "possibleUses": [
      "Generate alt text",
      "Summarize screenshots",
      "Extract document data"
    ]
  }
}
</code></pre>
<p>That combination (exact text with positions from Vision, plus a human-readable interpretation from the model) is enough to build real features on top of a searchable screenshot library indexed by <code>detectedText</code> and <code>suggestedTags</code>, automatic alt text for uploaded images, or click-to-highlight overlays powered by the bounding boxes.</p>
<p>And because the prompt lives in the companion, changing what comes back (say, extracting line items from receipts instead of tagging screenshots) is a prompt edit, not an architecture change.</p>
<h2 id="heading-run-the-app">Run the App</h2>
<p>Start the companion:</p>
<pre><code class="language-sh">npm run companion
</code></pre>
<p>In another terminal, start the web app:</p>
<pre><code class="language-sh">npm run dev
</code></pre>
<p>Open the Vite URL:</p>
<pre><code class="language-text">http://127.0.0.1:5173
</code></pre>
<p>If that port is busy, Vite will choose another one.</p>
<p>The companion should be available at:</p>
<pre><code class="language-text">http://127.0.0.1:43119
</code></pre>
<p>You can test it directly:</p>
<pre><code class="language-sh">curl http://127.0.0.1:43119/v1/health
</code></pre>
<p>Expected response:</p>
<pre><code class="language-json">{
  "app": "Vision Bridge Companion",
  "ok": true,
  "support": {
    "foundationModelAvailable": true,
    "foundationModelStatus": "available",
    "visionAvailable": true
  },
  "version": "0.1.0"
}
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/5db93b3da2342e8354088115/76a47c9a-934c-4133-ba7c-e2a9c6b6dad4.png" alt="Screenshot of terminal running companion" style="display:block;margin:0 auto" width="1448" height="556" loading="lazy">

<h2 id="heading-conclusion">Conclusion</h2>
<p>You now have a React interface that uploads an image, a Swift companion that analyzes it with Apple-native frameworks, and structured JSON flowing between them.</p>
<p>Vision Bridge is intentionally small, but the bridge itself is reusable. Once you have a trusted native companion, a web app can do more than send prompts to a remote model: it can ask the Mac to work with local context, use any Apple framework, and return structured data the browser can render, store, or sync.</p>
<h2 id="heading-resources">Resources</h2>
<ul>
<li><p><a href="https://developer.apple.com/documentation/foundationmodels">Apple Foundation Models documentation</a></p>
</li>
<li><p><a href="https://developer.apple.com/documentation/vision">Apple Vision documentation</a></p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <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>
        
            <item>
                <title>
                    <![CDATA[ How to Manage Secrets Securely with Azure Key Vault in Node.js ]]>
                </title>
                <description>
                    <![CDATA[ Last year a client called me about exactly this. Someone ran git log -p on a hunch and found a .env committed two years earlier, never caught. Database password, Stripe secret, JWT signing key — all s ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-manage-secrets-securely-with-azure-key-vault-in-node-js/</link>
                <guid isPermaLink="false">6a5e27b295e748bed9510853</guid>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Security ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Devops ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Azure ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Node.js ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Zia Ullah ]]>
                </dc:creator>
                <pubDate>Mon, 20 Jul 2026 13:50:42 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/5491b408-9c6b-4d4d-a53e-215119fb2d97.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Last year a client called me about exactly this. Someone ran <code>git log -p</code> on a hunch and found a <code>.env</code> committed two years earlier, never caught. Database password, Stripe secret, JWT signing key — all still active. All still in production.</p>
<p>IBM's 2024 breach cost report put the average data breach at <strong>$4.88 million</strong> — and that's the average, not the worst cases.</p>
<p>Exposed credentials are consistently near the top of root causes. GitHub found over a million secrets leaked in public repos in 2023 alone, before you even count the private ones nobody ever discovered.</p>
<p>It's not a people problem. The developers I've worked with aren't careless — the architecture is just set up to fail them. A <code>.env</code> file gets committed once by accident. Credentials get copied and pasted into a Slack message to unblock a teammate. A Docker image gets published with secrets baked into a layer. A server gets shut down, and nobody rotates the credentials it was holding.</p>
<p>Azure Key Vault solves this differently. Your application fetches credentials at runtime from a centralized, encrypted service — the <code>.env</code> file stops being a liability because it stops holding anything worth stealing.</p>
<p>What you'll build is a Node.js Express API that fetches every secret from Azure Key Vault at startup. No passwords in the code. When someone quits, there's nothing in the repo to rotate. The <code>.env</code> ends up with one line — the vault name.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<ul>
<li><p>Node.js 18+</p>
</li>
<li><p>An Azure account (free tier works)</p>
</li>
<li><p>Azure CLI installed and logged in (<code>az login</code>)</p>
</li>
<li><p>Basic knowledge of Express.js</p>
</li>
<li><p>Docker (optional — only needed for the local database test section)</p>
</li>
</ul>
<h2 id="heading-what-we-will-build">What We Will Build</h2>
<p>A Node.js Express API that:</p>
<ol>
<li><p>Connects to PostgreSQL using credentials fetched from Key Vault at startup</p>
</li>
<li><p>Uses Managed Identity for authentication — no client secrets or passwords anywhere</p>
</li>
<li><p>Caches secrets in memory, so Key Vault isn't called on every request</p>
</li>
<li><p>Works locally via Azure CLI auth and in production via Managed Identity — same code, zero changes</p>
</li>
</ol>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ol>
<li><p><a href="#heading-how-the-architecture-works">How the Architecture Works</a></p>
</li>
<li><p><a href="#heading-what-is-azure-key-vault">What Is Azure Key Vault?</a></p>
</li>
<li><p><a href="#heading-set-up-the-key-vault">Set Up the Key Vault</a></p>
</li>
<li><p><a href="#heading-create-the-nodejs-project">Create the Node.js Project</a></p>
</li>
<li><p><a href="#heading-connect-to-key-vault-with-managed-identity">Connect to Key Vault with Managed Identity</a></p>
</li>
<li><p><a href="#heading-cache-secrets-at-startup">Cache Secrets at Startup</a></p>
</li>
<li><p><a href="#heading-use-secrets-in-your-express-api">Use Secrets in Your Express API</a></p>
</li>
<li><p><a href="#heading-test-locally">Test Locally</a></p>
</li>
<li><p><a href="#heading-deploy-to-azure-app-service">Deploy to Azure App Service</a></p>
</li>
<li><p><a href="#heading-grant-key-vault-access-to-the-app">Grant Key Vault Access to the App</a></p>
</li>
<li><p><a href="#heading-rotate-secrets-without-redeploying">Rotate Secrets Without Redeploying</a></p>
</li>
<li><p><a href="#heading-troubleshooting">Troubleshooting</a></p>
</li>
<li><p><a href="#heading-wrapping-up">Wrapping Up</a></p>
</li>
</ol>
<h2 id="heading-how-the-architecture-works">How the Architecture Works</h2>
<p>Before writing any code, it helps to see the full picture:</p>
<pre><code class="language-plaintext"> LOCAL DEVELOPMENT
.-------------------------------------------------------.
|                                                        |
|   [Node.js App]                                        |
|        |                                               |
|        v                                               |
|   [DefaultAzureCredential] ---&gt; az login session       |
|        |                                               |
|        v                                               |
|   [Azure Key Vault]  ---&gt; Returns secrets              |
|        |                                               |
|        v                                               |
|   [In-memory cache]  ---&gt; App uses secrets at runtime  |
'-------------------------------------------------------'

 PRODUCTION (Azure)
.-------------------------------------------------------.
|                                                        |
|   [Azure App Service]                                  |
|        |                                               |
|        v                                               |
|   [DefaultAzureCredential] ---&gt; Managed Identity       |
|        |                                               |
|        v                                               |
|   [Azure Key Vault]  ---&gt; Returns secrets              |
|        |                                               |
|        v                                               |
|   [In-memory cache]  ---&gt; App uses secrets at runtime  |
'-------------------------------------------------------'
</code></pre>
<p>Both environments run the exact same code. <code>DefaultAzureCredential</code> figures out where it is — locally it picks up your <code>az login</code> session, on Azure it uses Managed Identity. You don't switch config files and you don't manage credentials. It just works.</p>
<h2 id="heading-what-is-azure-key-vault">What Is Azure Key Vault?</h2>
<p>Azure Key Vault is Microsoft's managed secret store — it handles secrets, keys, and certificates. For this tutorial, we're only using the secrets part: database passwords, API keys, JWT signing keys, anything your app needs to run but has no business being in your Git history.</p>
<p>Compared to <code>.env</code> files, the practical differences are worth understanding before you write any code.</p>
<p>Rotation is the one I notice most on real projects. Update a secret in Key Vault and every app picks it up on the next restart — no hunting down five different environment configs across staging and production.</p>
<p>Access control is the other big one. Each application only gets permission to read the secrets it actually needs. If one service gets compromised, it can't read credentials belonging to other services.</p>
<p>And every read gets logged. When something goes wrong — and eventually something will — you can see exactly which app accessed which secret, and when. That log is what auditors actually want to see.</p>
<p>I've sat in enough security reviews to know that "we use <code>.env</code> files and tell people not to commit them" doesn't satisfy an auditor. SOC 2, HIPAA, GDPR — they all want demonstrable controls. A vault with an access log is demonstrable.</p>
<h2 id="heading-set-up-the-key-vault">Set Up the Key Vault</h2>
<p>Run these commands. The vault name has to be globally unique across all of Azure — not just your own subscription — so pick something specific. Letters, numbers, and hyphens, 3 to 24 characters.</p>
<pre><code class="language-bash"># Create a resource group (skip if you already have one)
az group create \
  --name keyvault-demo-rg \
  --location eastus

# Create the Key Vault (RBAC enabled by default — required for the role assignment later)
az keyvault create \
  --name your-vault-name \
  --resource-group keyvault-demo-rg \
  --location eastus

# Grant yourself permission to manage secrets (required with RBAC — creators are not auto-assigned)
az role assignment create \
  --role "Key Vault Secrets Officer" \
  --assignee-object-id $(az ad signed-in-user show --query id -o tsv) \
  --scope $(az keyvault show \
    --name your-vault-name \
    --resource-group keyvault-demo-rg \
    --query id -o tsv)

# Add your secrets
az keyvault secret set \
  --vault-name your-vault-name \
  --name "DB-HOST" \
  --value "your-db-host.postgres.database.azure.com"

az keyvault secret set \
  --vault-name your-vault-name \
  --name "DB-PASSWORD" \
  --value "your-super-secret-password"

az keyvault secret set \
  --vault-name your-vault-name \
  --name "JWT-SECRET" \
  --value "your-jwt-signing-secret"
</code></pre>
<p>Verify the secrets were stored:</p>
<pre><code class="language-bash">az keyvault secret list --vault-name your-vault-name --query "[].name" -o tsv
</code></pre>
<p>You should see:</p>
<pre><code class="language-plaintext">DB-HOST
DB-PASSWORD
JWT-SECRET
</code></pre>
<h2 id="heading-create-the-nodejs-project">Create the Node.js Project</h2>
<p>Set up the project structure:</p>
<pre><code class="language-bash">mkdir nodejs-azure-keyvault
cd nodejs-azure-keyvault
npm init -y
npm install express pg jsonwebtoken @azure/keyvault-secrets @azure/identity dotenv
</code></pre>
<p>The two Azure packages do all the work:</p>
<ul>
<li><p><code>@azure/keyvault-secrets</code> — connects to your vault and pulls secrets out</p>
</li>
<li><p><code>@azure/identity</code> — handles auth. Locally, it uses your <code>az login</code> session, in production, it switches to Managed Identity automatically</p>
</li>
</ul>
<p>Add a start script to <code>package.json</code>:</p>
<pre><code class="language-bash">npm pkg set scripts.start="node server.js"
</code></pre>
<p>Create the following file structure:</p>
<pre><code class="language-plaintext">nodejs-azure-keyvault/
|-- src/
|   |-- config/
|   |   `-- secrets.js   # Key Vault client and secret loader
|   |-- db/
|   |   `-- index.js     # PostgreSQL pool using secrets
|   `-- routes/
|       `-- users.js     # Example route
|-- app.js               # Express app
`-- server.js            # Entry point -- loads secrets first
</code></pre>
<h2 id="heading-connect-to-key-vault-with-managed-identity">Connect to Key Vault with Managed Identity</h2>
<p>Create the secrets config file:</p>
<pre><code class="language-javascript">// src/config/secrets.js
const { SecretClient } = require('@azure/keyvault-secrets');
const { DefaultAzureCredential } = require('@azure/identity');

const VAULT_URL = `https://${process.env.KEY_VAULT_NAME}.vault.azure.net`;

const credential = new DefaultAzureCredential();
const client = new SecretClient(VAULT_URL, credential);

async function getSecret(name) {
  const secret = await client.getSecret(name);
  return secret.value;
}

module.exports = { getSecret };
</code></pre>
<p><code>DefaultAzureCredential</code> is the most important part of this setup. It tries a chain of authentication methods in order:</p>
<ol>
<li><p>Environment variables (for CI/CD pipelines)</p>
</li>
<li><p>Azure CLI credentials (for local development — <code>az login</code>)</p>
</li>
<li><p>Managed Identity (for deployed apps on Azure)</p>
</li>
</ol>
<p>This means the exact same code works locally and in production with zero changes. Locally, it uses your <code>az login</code> session. In production, it uses the app's Managed Identity. You never touch credentials.</p>
<h2 id="heading-cache-secrets-at-startup">Cache Secrets at Startup</h2>
<p>Calling Key Vault on every request adds latency and costs money. Load all secrets once at startup and cache them in memory. Replace <code>src/config/secrets.js</code> with this complete version:</p>
<pre><code class="language-javascript">// src/config/secrets.js
const { SecretClient } = require('@azure/keyvault-secrets');
const { DefaultAzureCredential } = require('@azure/identity');

const VAULT_URL = `https://${process.env.KEY_VAULT_NAME}.vault.azure.net`;

const credential = new DefaultAzureCredential();
const client = new SecretClient(VAULT_URL, credential);

// In-memory cache
const cache = {};

async function getSecret(name) {
  if (cache[name]) return cache[name];
  const secret = await client.getSecret(name);
  cache[name] = secret.value;
  return secret.value;
}

async function loadAllSecrets() {
  console.log('Loading secrets from Azure Key Vault...');
  const secretNames = ['DB-HOST', 'DB-PASSWORD', 'JWT-SECRET'];

  await Promise.all(
    secretNames.map(async (name) =&gt; {
      cache[name] = await getSecret(name);
      console.log(`  ✓ ${name} loaded`);
    })
  );

  console.log('All secrets loaded successfully.');
}

function getFromCache(name) {
  if (!cache[name]) throw new Error(`Secret "${name}" not loaded. Did loadAllSecrets() run?`);
  return cache[name];
}

module.exports = { loadAllSecrets, getFromCache };
</code></pre>
<p>The <code>loadAllSecrets</code> function runs once when the application starts. After that, all secrets are served from the in-memory cache with zero latency and zero Key Vault calls.</p>
<h2 id="heading-use-secrets-in-your-express-api">Use Secrets in Your Express API</h2>
<p>Set up the database connection using the cached secrets:</p>
<pre><code class="language-javascript">// src/db/index.js
const { Pool } = require('pg');
const { getFromCache } = require('../config/secrets');

let pool;

function getPool() {
  if (!pool) {
    pool = new Pool({
      host:     getFromCache('DB-HOST'),
      database: process.env.DB_NAME || 'myapp',
      user:     process.env.DB_USER || 'dbadmin',
      password: getFromCache('DB-PASSWORD'),
      port:     parseInt(process.env.DB_PORT || '5432'),
      ssl:      process.env.NODE_ENV === 'production'
                  ? { rejectUnauthorized: false }
                  : false,
    });

    pool.on('error', (err) =&gt; {
      console.error('Unexpected database pool error:', err.message);
    });
  }

  return pool;
}

module.exports = { getPool };
</code></pre>
<p>Notice the distinction: <code>DB-HOST</code> and <code>DB-PASSWORD</code> come from Key Vault because they're sensitive. The database name, username, and port are not — they don't need to be protected, so they use environment variables with sensible defaults. Key Vault is for credentials, not all configuration.</p>
<p>The SSL flag is environment-aware: forced on in production, off locally so Docker connections work without a certificate. The <code>rejectUnauthorized: false</code> setting accepts Azure Database for PostgreSQL's certificate without verifying the CA chain — this is standard for Azure-managed databases. For stricter environments, you can download the Azure root CA and pass it via the <code>ca</code> option in the pool config instead.</p>
<p>Create a sample route that uses JWT verification with the secret from Key Vault:</p>
<pre><code class="language-javascript">// src/routes/users.js
const express = require('express');
const jwt     = require('jsonwebtoken');
const { getFromCache } = require('../config/secrets');
const { getPool }      = require('../db');

const router = express.Router();

// Auth middleware — JWT secret comes from Key Vault, not process.env
function authMiddleware(req, res, next) {
  const authHeader = req.headers.authorization;
  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Missing or malformed Authorization header' });
  }

  const token = authHeader.split(' ')[1];

  try {
    req.user = jwt.verify(token, getFromCache('JWT-SECRET'));
    next();
  } catch (err) {
    return res.status(401).json({ error: 'Invalid or expired token' });
  }
}

// GET /api/users — list users (authenticated)
router.get('/', authMiddleware, async (req, res) =&gt; {
  try {
    const result = await getPool().query(
      'SELECT id, email, created_at FROM users ORDER BY created_at DESC LIMIT 20'
    );
    res.json(result.rows);
  } catch (err) {
    console.error('Database error:', err.message);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// GET /api/users/:id — single user (authenticated)
router.get('/:id', authMiddleware, async (req, res) =&gt; {
  try {
    const result = await getPool().query(
      'SELECT id, email, created_at FROM users WHERE id = $1',
      [req.params.id]
    );
    if (!result.rows[0]) return res.status(404).json({ error: 'User not found' });
    res.json(result.rows[0]);
  } catch (err) {
    console.error('Database error:', err.message);
    res.status(500).json({ error: 'Internal server error' });
  }
});

module.exports = router;
</code></pre>
<p>Notice the error handler returns <code>'Internal server error'</code> instead of <code>err.message</code>. Database errors are surprisingly chatty — they'll hand an attacker your table names, column names, and query structure if you let them through.</p>
<p>Set up the Express application. Both files define <code>authMiddleware</code> locally — yes, it's duplicated. In production, I'd pull this into a shared middleware file. For this tutorial, keeping it local means you can read either file without bouncing between three others:</p>
<pre><code class="language-javascript">// app.js
const express = require('express');
const jwt = require('jsonwebtoken');
const { getFromCache } = require('./src/config/secrets');
const usersRouter = require('./src/routes/users');

const app = express();
app.use(express.json());

// Auth middleware — JWT secret comes from Key Vault, not process.env
function authMiddleware(req, res, next) {
  const authHeader = req.headers.authorization;
  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Missing or malformed Authorization header' });
  }
  const token = authHeader.split(' ')[1];
  try {
    req.user = jwt.verify(token, getFromCache('JWT-SECRET'));
    next();
  } catch (err) {
    return res.status(401).json({ error: 'Invalid or expired token' });
  }
}

// Health check — no auth required
app.get('/health', (req, res) =&gt; {
  res.json({ status: 'healthy', timestamp: new Date().toISOString() });
});

// Status endpoint — proves Key Vault integration without needing a database
app.get('/api/status', authMiddleware, (req, res) =&gt; {
  res.json({
    message: 'All secrets loaded from Azure Key Vault',
    vault: process.env.KEY_VAULT_NAME,
    secrets_loaded: ['DB-HOST', 'DB-PASSWORD', 'JWT-SECRET'],
    authenticated_as: req.user.email,
    timestamp: new Date().toISOString()
  });
});

app.use('/api/users', usersRouter);

app.use((req, res) =&gt; res.status(404).json({ error: 'Route not found' }));
app.use((err, req, res, next) =&gt; {
  console.error('Unhandled error:', err.message);
  res.status(500).json({ error: 'Internal server error' });
});

module.exports = app;
</code></pre>
<p>The entry point loads secrets before starting the server. The server doesn't start unless all secrets load successfully:</p>
<pre><code class="language-javascript">// server.js
require('dotenv').config();
const app = require('./app');
const { loadAllSecrets } = require('./src/config/secrets');

const PORT = process.env.PORT || 3000;

async function start() {
  try {
    await loadAllSecrets();
    app.listen(PORT, () =&gt; {
      console.log(`Server running on port ${PORT}`);
    });
  } catch (err) {
    console.error('Failed to start server:', err.message);
    console.error('Hint: Run "az login" for local development, or check Managed Identity for Azure deployments.');
    process.exit(1);
  }
}

start();
</code></pre>
<p>That <code>process.exit(1)</code> is deliberate. I'd rather the app crash loudly at startup than limp along with missing credentials and fail on the first real request two hours later.</p>
<h2 id="heading-test-locally">Test Locally</h2>
<p>Create a <code>.env</code> file for local development. This only contains the Key Vault name, nothing sensitive:</p>
<pre><code class="language-bash"># .env
KEY_VAULT_NAME=your-vault-name
PORT=3000
</code></pre>
<p>Add <code>.env</code> and the deployment zip to <code>.gitignore</code>:</p>
<pre><code class="language-bash">echo ".env" &gt;&gt; .gitignore
echo "app.zip" &gt;&gt; .gitignore
</code></pre>
<p>Make sure you're logged into Azure CLI:</p>
<pre><code class="language-bash">az login
</code></pre>
<p>Start the application:</p>
<pre><code class="language-bash">npm start
</code></pre>
<p>You should see:</p>
<pre><code class="language-plaintext">Loading secrets from Azure Key Vault...
  ✓ JWT-SECRET loaded
  ✓ DB-PASSWORD loaded
  ✓ DB-HOST loaded
All secrets loaded successfully.
Server running on port 3000
</code></pre>
<p>The order secrets load may vary — <code>Promise.all</code> fetches them in parallel and resolves as each one completes. What matters is that all three are confirmed before the server starts.</p>
<p>Test the health endpoint:</p>
<pre><code class="language-bash">curl http://localhost:3000/health
# {"status":"healthy","timestamp":"2026-07-14T19:38:11.659Z"}
</code></pre>
<p>Now prove the integration end-to-end. Grab the value you stored as <code>JWT-SECRET</code> and use it to sign a test token — paste it in for <code>YOUR-JWT-SECRET-VALUE</code>. Then hit <code>/api/status</code> with it:</p>
<pre><code class="language-bash">node -e "const jwt = require('jsonwebtoken'); console.log(jwt.sign({id:1, email:'test@test.com'}, 'YOUR-JWT-SECRET-VALUE', {expiresIn:'1h'}));"
</code></pre>
<p>On Linux/macOS:</p>
<pre><code class="language-bash">curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:3000/api/status
</code></pre>
<p>On Windows PowerShell:</p>
<pre><code class="language-powershell">Invoke-RestMethod -Uri "http://localhost:3000/api/status" -Headers @{Authorization = "Bearer YOUR_TOKEN"}
</code></pre>
<p>You should see:</p>
<pre><code class="language-json">{
  "message": "All secrets loaded from Azure Key Vault",
  "vault": "your-vault-name",
  "secrets_loaded": ["DB-HOST", "DB-PASSWORD", "JWT-SECRET"],
  "authenticated_as": "test@test.com",
  "timestamp": "2026-07-14T19:50:08.687Z"
}
</code></pre>
<p>If you got that response, the whole chain worked. The JWT was signed and verified using a secret that lived only in Key Vault — not in your code, not in your<code>.env</code>, not anywhere in the repo. Your <code>az login</code> session handled the auth locally. In production, Managed Identity takes over. Same code, nothing changes.</p>
<h3 id="heading-test-the-full-database-flow-with-docker">Test the Full Database Flow with Docker</h3>
<p>The app reads <code>DB-HOST</code> and <code>DB-PASSWORD</code> from Key Vault, so those secrets need to match your local Docker container. Update them now:</p>
<pre><code class="language-bash">az keyvault secret set --vault-name your-vault-name --name "DB-HOST" --value "localhost"
az keyvault secret set --vault-name your-vault-name --name "DB-PASSWORD" --value "demopassword123"
</code></pre>
<p>Docker up a Postgres container. The password has to match <code>demopassword123</code> — that's what you just put in Key Vault:</p>
<pre><code class="language-bash">docker run --name pg-demo \
  -e POSTGRES_USER=dbadmin \
  -e POSTGRES_PASSWORD=demopassword123 \
  -e POSTGRES_DB=myapp \
  -p 5432:5432 \
  -d postgres:15
</code></pre>
<p>Get the table created and throw in some test rows:</p>
<pre><code class="language-bash">docker exec -it pg-demo psql -U dbadmin -d myapp -c \
  "CREATE TABLE IF NOT EXISTS users (id SERIAL PRIMARY KEY, email VARCHAR(255) UNIQUE NOT NULL, created_at TIMESTAMPTZ DEFAULT NOW());"

docker exec -it pg-demo psql -U dbadmin -d myapp -c \
  "INSERT INTO users (email) VALUES ('alice@example.com'), ('bob@example.com'), ('carol@example.com');"
</code></pre>
<p>Kill the server and bring it back up — secrets load at startup, so it needs a fresh run to pick up what you just changed in Key Vault:</p>
<pre><code class="language-bash">npm start
</code></pre>
<p>Call the users endpoint with a valid JWT:</p>
<pre><code class="language-bash"># Generate a token (use the same value you stored as JWT-SECRET in Key Vault)
node -e "const jwt = require('jsonwebtoken'); console.log(jwt.sign({id:1, email:'test@test.com'}, 'YOUR-JWT-SECRET-VALUE', {expiresIn:'1h'}));"
</code></pre>
<p>On Linux/macOS:</p>
<pre><code class="language-bash">curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:3000/api/users
</code></pre>
<p>On Windows PowerShell:</p>
<pre><code class="language-powershell">Invoke-RestMethod -Uri "http://localhost:3000/api/users" -Headers @{Authorization = "Bearer YOUR_TOKEN"}
</code></pre>
<p>You should see:</p>
<pre><code class="language-json">[
  { "id": 1, "email": "alice@example.com", "created_at": "2026-07-14T19:59:21.064Z" },
  { "id": 2, "email": "bob@example.com",   "created_at": "2026-07-14T19:59:21.064Z" },
  { "id": 3, "email": "carol@example.com", "created_at": "2026-07-14T19:59:21.064Z" }
]
</code></pre>
<p>That query ran using a password that came straight from Key Vault. It's not in your <code>.env</code>, not hardcoded anywhere, and not in a local variable. The repo has nothing worth stealing.</p>
<p>Before you deploy, put the real production values back in Key Vault:</p>
<pre><code class="language-bash">az keyvault secret set --vault-name your-vault-name --name "DB-HOST" --value "your-db-host.postgres.database.azure.com"
az keyvault secret set --vault-name your-vault-name --name "DB-PASSWORD" --value "your-super-secret-password"
</code></pre>
<p>If you skip this, the deployed app will try to connect to <code>localhost</code> and fail immediately — <code>localhost</code> doesn't exist on App Service.</p>
<h2 id="heading-deploy-to-azure-app-service">Deploy to Azure App Service</h2>
<p><strong>Note:</strong> This section creates the App Service infrastructure. The actual code deployment (zip upload) happens at the end of the next section — the app must have Key Vault access configured before its first startup, or it will fail immediately and exit.</p>
<p>Create the App Service:</p>
<pre><code class="language-bash"># Create an App Service Plan (B1 is the cheapest paid tier)
az appservice plan create \
  --name keyvault-demo-plan \
  --resource-group keyvault-demo-rg \
  --sku B1 \
  --is-linux

# Create the Web App
az webapp create \
  --name my-keyvault-node-app \
  --resource-group keyvault-demo-rg \
  --plan keyvault-demo-plan \
  --runtime "NODE:18-lts"

# Set app settings — KEY_VAULT_NAME tells the app which vault to use
# NODE_ENV=production enables SSL for the database connection
az webapp config appsettings set \
  --name my-keyvault-node-app \
  --resource-group keyvault-demo-rg \
  --settings KEY_VAULT_NAME=your-vault-name NODE_ENV=production
</code></pre>
<h2 id="heading-grant-key-vault-access-to-the-app">Grant Key Vault Access to the App</h2>
<p>Enable Managed Identity on the app. This gives it an identity in Microsoft Entra ID that Key Vault can trust:</p>
<pre><code class="language-bash"># Enable system-assigned managed identity
az webapp identity assign \
  --name my-keyvault-node-app \
  --resource-group keyvault-demo-rg
</code></pre>
<p>The following commands capture the <code>principalId</code> automatically and use it to grant the role:</p>
<pre><code class="language-bash"># Get the principal ID
PRINCIPAL_ID=$(az webapp identity show \
  --name my-keyvault-node-app \
  --resource-group keyvault-demo-rg \
  --query principalId \
  --output tsv)

# Get the Key Vault resource ID
KV_ID=$(az keyvault show \
  --name your-vault-name \
  --resource-group keyvault-demo-rg \
  --query id \
  --output tsv)

# Grant the app the "Key Vault Secrets User" role
az role assignment create \
  --role "Key Vault Secrets User" \
  --assignee-object-id $PRINCIPAL_ID \
  --scope $KV_ID
</code></pre>
<p>The <code>Key Vault Secrets User</code> role allows the app to read secrets. It can't create, update, or delete them. This is the principle of least privilege — the application can only do what it needs to do.</p>
<p>Time to ship it. Linux/macOS can run this directly — Windows users, open Git Bash (it ships with Git for Windows):</p>
<pre><code class="language-bash">zip -r app.zip . -x "node_modules/*" ".git/*" ".env" "app.zip"
</code></pre>
<p>Then deploy:</p>
<pre><code class="language-bash">az webapp deployment source config-zip \
  --name my-keyvault-node-app \
  --resource-group keyvault-demo-rg \
  --src app.zip
</code></pre>
<p>The deployed application authenticates to Key Vault using its Managed Identity automatically. No passwords, no client secrets, no credentials of any kind in the deployment.</p>
<p>Check the health endpoint to confirm it's running:</p>
<pre><code class="language-bash">curl https://my-keyvault-node-app.azurewebsites.net/health
# {"status":"healthy","timestamp":"..."}
</code></pre>
<p>If it won't start, pull the logs:</p>
<pre><code class="language-bash">az webapp log tail --name my-keyvault-node-app --resource-group keyvault-demo-rg
</code></pre>
<p>Nine times out of ten, it's that the Key Vault role assignment has not been propagated yet. Give it 2–3 minutes, then restart:</p>
<pre><code class="language-bash">az webapp restart --name my-keyvault-node-app --resource-group keyvault-demo-rg
</code></pre>
<h2 id="heading-rotate-secrets-without-redeploying">Rotate Secrets Without Redeploying</h2>
<p>One of the biggest practical benefits of Key Vault is secret rotation. When a database password needs to change, you update it in Key Vault — not in your app:</p>
<pre><code class="language-bash">az keyvault secret set \
  --vault-name your-vault-name \
  --name "DB-PASSWORD" \
  --value "new-rotated-password"
</code></pre>
<p>The cache builds at startup, so you don't need a redeploy — a restart is enough:</p>
<pre><code class="language-bash">az webapp restart \
  --name my-keyvault-node-app \
  --resource-group keyvault-demo-rg
</code></pre>
<p>No code change. No new deployment. The secret is rotated, and the app is using the new value in seconds.</p>
<p>If you need zero-downtime rotation, add a <code>/refresh-secrets</code> endpoint behind admin auth that clears the cache and then calls <code>loadAllSecrets()</code>. The order matters — <code>loadAllSecrets()</code> uses <code>getSecret()</code> which returns cached values if they exist, so you must clear the cache first, or it will reload nothing. This is optional but useful for long-running processes that can't afford a restart.</p>
<h2 id="heading-troubleshooting">Troubleshooting</h2>
<p><code>CredentialUnavailableError: DefaultAzureCredential failed to retrieve a token</code></p>
<p>You're not logged into Azure CLI. Run <code>az login</code> and try again. On Azure App Service, check that Managed Identity is enabled and the role assignment was created correctly.</p>
<p><code>RestError: Forbidden — The user does not have secrets get permission</code></p>
<p>The Managed Identity isn't wired up to Key Vault yet. Go back and run the <code>az role assignment create</code> command. If you already did, it might just need time. Azure can take 2–3 minutes to propagate role assignments, so give it a moment before you dig further.</p>
<p><code>Error: Secret "DB-PASSWORD" not loaded. Did loadAllSecrets() run?</code></p>
<p><code>getFromCache()</code> ran before <code>loadAllSecrets()</code> finished, meaning the startup sequence is out of order. Open <code>server.js</code> and confirm <code>await loadAllSecrets()</code> comes before <code>app.listen()</code>. If the order's fine, the secret might just not be in the vault yet. Run <code>az keyvault secret list --vault-name YOUR_VAULT</code> to double-check. (A name mismatch — wrong case, typo — throws <code>SecretNotFound</code> instead, which is the entry below.)</p>
<p><strong>App starts locally but fails on Azure App Service</strong></p>
<p>Almost always, the app setting. Either <code>KEY_VAULT_NAME</code> isn't in App Service configuration at all, or the vault name has a typo. Run <code>az webapp log tail</code> to see the actual startup error — that'll tell you which one.</p>
<p><code>AuthorizationFailed</code> <strong>when running</strong> <code>az role assignment create</code></p>
<p>You are a guest user in your Azure tenant and lack the Owner role needed to assign roles. Switch the existing vault to the access policy model — no need to recreate it or lose your secrets:</p>
<pre><code class="language-bash">az keyvault update \
  --name your-vault-name \
  --resource-group keyvault-demo-rg \
  --enable-rbac-authorization false
</code></pre>
<p>If this happened during <strong>Set Up the Key Vault</strong> (granting yourself access), run:</p>
<pre><code class="language-bash">az keyvault set-policy \
  --name your-vault-name \
  --object-id $(az ad signed-in-user show --query id -o tsv) \
  --secret-permissions get set list delete
</code></pre>
<p>If this happened during <strong>Grant Key Vault Access to the App</strong> (granting the Managed Identity access), run:</p>
<pre><code class="language-bash">az keyvault set-policy \
  --name your-vault-name \
  --object-id $PRINCIPAL_ID \
  --secret-permissions get list
</code></pre>
<p><strong>Key Vault returns</strong> <code>SecretNotFound</code></p>
<p>The secret was never added, was deleted, or its name doesn't match exactly what your code requests — Key Vault secret names are case-sensitive. A secret named <code>db-password</code> and a request for <code>DB-PASSWORD</code> are different names. Run <code>az keyvault secret list --vault-name YOUR_VAULT</code> and compare what's actually in the vault against what <code>loadAllSecrets()</code> is asking for in <code>src/config/secrets.js</code>. Usually, it's a casing issue or a stray hyphen.</p>
<h2 id="heading-wrapping-up">Wrapping Up</h2>
<p>The <code>.env</code> file in this project contains exactly one value: the Key Vault name. That's not sensitive. Every actual secret — database passwords, API keys, signing secrets — lives in Key Vault and never touches your codebase or your deployment pipeline.</p>
<p>This is the pattern I use on Azure projects now. The startup check is the part I find most useful in practice: if Key Vault is unreachable or a secret is missing, the server exits immediately with a clear error instead of starting up broken and failing on the first real request. You find out right away, rather than getting an obscure database connection error two hours later.</p>
<p>To add another secret, put it in Key Vault and drop its name into the <code>secretNames</code> array — that's it. Everything else scales with it.</p>
<p>The full working code is on GitHub: <a href="https://github.com/ziaongit/nodejs-azure-keyvault">nodejs-azure-keyvault</a></p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Browser-Based PDF Redaction Tool Using JavaScript ]]>
                </title>
                <description>
                    <![CDATA[ PDF documents are frequently used to share invoices, contracts, reports, legal records, customer documents, financial statements, and internal business files. But before these documents are shared, th ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-pdf-redaction-tool-javascript/</link>
                <guid isPermaLink="false">6a5a91870c25cb6dfa32c0c9</guid>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ pdf ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Online PDF Tools ]]>
                    </category>
                
                    <category>
                        <![CDATA[ pdf tutorial ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Bhavin Sheth ]]>
                </dc:creator>
                <pubDate>Fri, 17 Jul 2026 20:33:11 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/18191d9d-9abf-44a3-8330-e452ce7194c2.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>PDF documents are frequently used to share invoices, contracts, reports, legal records, customer documents, financial statements, and internal business files. But before these documents are shared, they may contain information that shouldn't be visible to the recipient.</p>
<p>An invoice might include an account number. A customer document may contain a home address or phone number. A legal file could reveal confidential case information, while an internal report may contain names, references, or business data intended only for employees.</p>
<p>This is where PDF redaction becomes useful.</p>
<p>Redaction allows users to select sensitive areas of a document and permanently cover those areas before creating a new PDF. A practical redaction tool should also support multiple redaction areas, page selection, document preview, and final output verification.</p>
<p>In this tutorial, you'll build a browser-based PDF Redaction Tool using JavaScript. Users will upload a PDF, navigate through its pages, draw redaction boxes directly on the document preview, manage multiple redactions, apply them to selected pages, preview the processed document, rename the final file, and download the redacted PDF.</p>
<p>The entire workflow runs inside the browser. This is particularly useful for privacy-focused document tools because PDF processing can happen locally without requiring a backend server.</p>
<p>By the end of this tutorial, you'll understand not only how to draw redaction areas but also how to translate browser coordinates into PDF coordinates and apply those selections to the actual document.</p>
<h3 id="heading-table-of-contents">Table of Contents</h3>
<ul>
<li><p><a href="#heading-redaction-is-not-the-same-as-drawing-a-black-box">Redaction Is Not the Same as Drawing a Black Box</a></p>
</li>
<li><p><a href="#heading-how-browser-based-pdf-redaction-works">How Browser-Based PDF Redaction Works</a></p>
</li>
<li><p><a href="#heading-understanding-pdf-and-canvas-coordinates">Understanding PDF and Canvas Coordinates</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-creating-the-pdf-upload-interface">Creating the PDF Upload Interface</a></p>
</li>
<li><p><a href="#heading-previewing-uploaded-pdf-pages">Previewing Uploaded PDF Pages</a></p>
</li>
<li><p><a href="#heading-drawing-redaction-areas-on-the-pdf">Drawing Redaction Areas on the PDF</a></p>
</li>
<li><p><a href="#heading-storing-and-managing-redactions">Storing and Managing Redactions</a></p>
</li>
<li><p><a href="#heading-applying-redactions-to-selected-pages">Applying Redactions to Selected Pages</a></p>
</li>
<li><p><a href="#heading-generating-the-redacted-pdf">Generating the Redacted PDF</a></p>
</li>
<li><p><a href="#heading-previewing-and-renaming-the-final-pdf">Previewing and Renaming the Final PDF</a></p>
</li>
<li><p><a href="#heading-downloading-the-final-pdf">Downloading the Final PDF</a></p>
</li>
<li><p><a href="#heading-demo-how-the-pdf-redaction-tool-works">Demo: How the PDF Redaction Tool Works</a></p>
</li>
<li><p><a href="#heading-how-to-verify-the-redacted-pdf">How to Verify the Redacted PDF</a></p>
</li>
<li><p><a href="#heading-performance-optimization-tips">Performance Optimization Tips</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-redaction-is-not-the-same-as-drawing-a-black-box">Redaction Is Not the Same as Drawing a Black Box</h2>
<p>A common mistake is assuming that placing a black rectangle over text automatically makes the information secure.</p>
<p>Visually, the document may look redacted. But depending on how the PDF is modified, the original text or image content may still exist underneath the rectangle.</p>
<p>For example, imagine adding a black box as a new annotation layer above an account number. The number is no longer visible on the page, but the underlying PDF content may still be present.</p>
<p>In some poorly redacted documents, users may be able to select, copy, search, or recover the hidden content.</p>
<p>This is why redaction must be treated differently from simple visual decoration.</p>
<p>In our browser-based workflow, the selected areas are applied while generating the processed PDF. The final document should then be reviewed carefully before it's shared.</p>
<p>Never assume that a black rectangle alone guarantees secure removal of underlying PDF content. For high-security or legally sensitive documents, the final file should be validated with a dedicated redaction verification process.</p>
<h2 id="heading-how-browser-based-pdf-redaction-works">How Browser-Based PDF Redaction Works</h2>
<p>The redaction workflow can be divided into a few clear stages.</p>
<p>First, the browser reads the uploaded PDF and renders a page preview. The preview gives users a visual surface where they can identify sensitive information.</p>
<p>Next, users click and drag over the preview to create redaction rectangles.</p>
<p>Each rectangle is stored as a set of coordinates.</p>
<pre><code class="language-javascript">const redaction = {
    page: 7,
    x: 420,
    y: 35,
    width: 310,
    height: 220
};
</code></pre>
<p>The application can store multiple rectangles for the same page.</p>
<pre><code class="language-javascript">redactions.push(redaction);
</code></pre>
<p>When users click <strong>Apply &amp; Finalize</strong>, the application determines which pages should receive the selected redactions.</p>
<p>The redaction coordinates are then converted from preview coordinates to actual PDF page coordinates. Finally, the application modifies the PDF and generates a new document for preview and download.</p>
<p>The overall workflow looks like this:</p>
<pre><code class="language-text">Upload PDF
    ↓
Render Page Preview
    ↓
Draw Redaction Areas
    ↓
Store Coordinates
    ↓
Select Target Pages
    ↓
Apply Redactions
    ↓
Generate New PDF
    ↓
Preview and Download
</code></pre>
<p>Separating the interface from the PDF processing logic makes the application easier to manage and debug.</p>
<h2 id="heading-understanding-pdf-and-canvas-coordinates">Understanding PDF and Canvas Coordinates</h2>
<p>One of the most important technical parts of this project is coordinate conversion.</p>
<p>The PDF page shown inside the browser is usually scaled to fit the available screen space. A PDF page may have an actual width of 842 points, while the browser preview is displayed at only 600 pixels wide.</p>
<p>This means a rectangle drawn at <code>x = 300</code> on the preview can't simply be placed at <code>x = 300</code> in the PDF.</p>
<p>We'll first calculate the scale difference.</p>
<pre><code class="language-javascript">const scaleX =
    pdfPageWidth / canvasWidth;

const scaleY =
    pdfPageHeight / canvasHeight;
</code></pre>
<p>The selected rectangle can then be converted.</p>
<pre><code class="language-javascript">const pdfX =
    redaction.x * scaleX;

const pdfWidth =
    redaction.width * scaleX;

const pdfHeight =
    redaction.height * scaleY;
</code></pre>
<p>The Y coordinate requires extra attention because browser canvases and PDF pages commonly use different coordinate origins.</p>
<p>Canvas coordinates generally begin at the top-left corner. PDF coordinates commonly work from the bottom-left.</p>
<p>The Y position can therefore be converted like this:</p>
<pre><code class="language-javascript">const pdfY =
    pdfPageHeight -
    ((redaction.y + redaction.height) * scaleY);
</code></pre>
<p>This small calculation is critical.</p>
<p>Without correct coordinate conversion, a redaction box drawn over a phone number might appear several centimeters away from that number in the generated PDF.</p>
<p>Accurate coordinate mapping ensures that the redaction users draw in the browser matches the same area in the final document.</p>
<h2 id="heading-project-setup">Project Setup</h2>
<p>We'll keep the project structure simple because the redaction workflow runs entirely inside the browser.</p>
<p>Create a new project folder with three files:</p>
<pre><code class="language-text">pdf-redaction-tool/
│
├── index.html
├── style.css
└── script.js
</code></pre>
<p>The <code>index.html</code> file contains the upload interface, PDF preview, redaction controls, and final download section.</p>
<p>The <code>style.css</code> file handles the page layout and redaction overlay styling.</p>
<p>The <code>script.js</code> file contains the PDF loading, rendering, coordinate tracking, redaction management, and final PDF generation logic.</p>
<p>Start with a 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 Redaction Tool&lt;/title&gt;

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

&lt;body&gt;

    &lt;main id="app"&gt;

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

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

        &lt;section id="resultSection"&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>Separating the upload, editor, and result sections makes it easier to show and hide different parts of the interface as users move through the redaction workflow.</p>
<h2 id="heading-what-libraries-are-we-using">What Libraries Are We Using?</h2>
<p>We'll use <strong>PDF.js</strong> and <strong>PDF-lib</strong> for this project.</p>
<p>PDF.js handles PDF loading and page rendering. It allows us to display an uploaded PDF page inside a canvas so users can visually select the areas they want to redact.</p>
<p>PDF-lib handles the final document modification. After the user creates redaction areas, PDF-lib opens the original PDF, accesses the selected pages, and applies the redaction rectangles before generating a new file.</p>
<p>Add both libraries before your main JavaScript file.</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://cdn.jsdelivr.net/npm/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.</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>We'll also create a few variables for storing the active document and redaction data.</p>
<pre><code class="language-javascript">let pdfDocument = null;

let pdfBytes = null;

let currentPage = 1;

let redactions = {};
</code></pre>
<p>Instead of storing every redaction in one flat array, we can organize them by page number.</p>
<pre><code class="language-javascript">redactions = {
    1: [],
    2: [],
    7: []
};
</code></pre>
<p>This structure makes page navigation and per-page redaction management much easier.</p>
<h2 id="heading-creating-the-pdf-upload-interface">Creating the PDF Upload Interface</h2>
<p>The first screen users see is the PDF upload area.</p>
<p>Users can drag a document onto the upload box or click the <strong>Select PDF</strong> button to open the browser's file picker.</p>
<p>Create the upload interface.</p>
<pre><code class="language-html">&lt;section id="uploadSection"&gt;

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

    &lt;p&gt;
        Upload your PDF to permanently black out
        sensitive information.
    &lt;/p&gt;

    &lt;div id="dropZone" class="drop-zone"&gt;

        &lt;div class="upload-icon"&gt;☁&lt;/div&gt;

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

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

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

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

    &lt;/div&gt;

&lt;/section&gt;
</code></pre>
<p>Connect the button to the hidden file input.</p>
<pre><code class="language-javascript">const pdfInput =
    document.getElementById("pdfInput");

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

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

    pdfInput.click();

});
</code></pre>
<p>Next, listen for file selection.</p>
<pre><code class="language-javascript">pdfInput.addEventListener("change", async event =&gt; {

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

    if (!file) {
        return;
    }

    await loadPdfFile(file);

});
</code></pre>
<p>Before loading the document, validate the selected file.</p>
<pre><code class="language-javascript">async function loadPdfFile(file) {

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

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

        return;

    }

    pdfBytes =
        await file.arrayBuffer();

}
</code></pre>
<p>We can also support drag-and-drop uploads.</p>
<pre><code class="language-javascript">dropZone.addEventListener("dragover", event =&gt; {

    event.preventDefault();

    dropZone.classList.add("dragging");

});

dropZone.addEventListener("dragleave", () =&gt; {

    dropZone.classList.remove("dragging");

});

dropZone.addEventListener("drop", async event =&gt; {

    event.preventDefault();

    dropZone.classList.remove("dragging");

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

    if (file) {

        await loadPdfFile(file);

    }

});
</code></pre>
<p>After reading the file, load it with PDF.js.</p>
<pre><code class="language-javascript">pdfDocument =
    await pdfjsLib
        .getDocument({
            data: pdfBytes.slice(0)
        })
        .promise;

currentPage = 1;

await renderPage(currentPage);
</code></pre>
<p>At this point, the document is ready for preview and redaction.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/0a6bd531-08d2-4619-a84a-512cc3a4a698.png" alt="PDF Redaction Tool upload interface with drag-and-drop area and Select PDF button." style="display:block;margin:0 auto" width="640" height="654" loading="lazy">

<h2 id="heading-previewing-uploaded-pdf-pages">Previewing Uploaded PDF Pages</h2>
<p>Once the PDF has loaded, the application displays the current page inside a canvas.</p>
<p>The canvas serves two purposes. First, it gives users an accurate preview of the document. Second, it becomes the visual surface where redaction rectangles will be drawn.</p>
<p>Create the editor preview.</p>
<pre><code class="language-html">&lt;section id="editorSection"&gt;

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

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

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

            &lt;div id="redactionLayer"&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;/section&gt;
</code></pre>
<p>The <code>pdfCanvas</code> displays the PDF page.</p>
<p>The <code>redactionLayer</code> sits above the canvas and contains the selection boxes created by the user.</p>
<p>Render the active page using PDF.js.</p>
<pre><code class="language-javascript">async function renderPage(pageNumber) {

    const page =
        await pdfDocument.getPage(pageNumber);

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

    const canvas =
        document.getElementById("pdfCanvas");

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

    canvas.width =
        viewport.width;

    canvas.height =
        viewport.height;

    await page.render({

        canvasContext: context,

        viewport: viewport

    }).promise;

    updatePageInformation();

    renderSavedRedactions();

}
</code></pre>
<p>Update the page navigation information.</p>
<pre><code class="language-javascript">function updatePageInformation() {

    document
        .getElementById("pageInfo")
        .textContent =
        `Page ${currentPage} of ${pdfDocument.numPages}`;

}
</code></pre>
<p>Users can move to the previous page.</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-page button works in the same way.</p>
<pre><code class="language-javascript">nextPage.addEventListener("click", async () =&gt; {

    if (
        currentPage &gt;= pdfDocument.numPages
    ) {
        return;
    }

    currentPage++;

    await renderPage(currentPage);

});
</code></pre>
<p>Whenever users change pages, the canvas renders the selected PDF page and restores any redaction boxes already saved for that page.</p>
<p>This is important because redactions are page-specific. A rectangle created on page 7 should not automatically appear on page 8 unless the user later chooses to apply that selection to multiple pages.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/1cd5c990-ba70-490e-8904-6870d9c7c38c.png" alt="Alt Text: Uploaded PDF page preview with previous and next page navigation controls in the PDF Redaction Tool." style="display:block;margin:0 auto" width="642" height="539" loading="lazy">

<p>The PDF is now loaded, rendered, and ready for user interaction.</p>
<h2 id="heading-drawing-redaction-areas-on-the-pdf">Drawing Redaction Areas on the PDF</h2>
<p>Now that the PDF page is visible, users need a simple way to mark the information they want to hide.</p>
<p>In this project, users can click and drag directly over the page preview to create a redaction rectangle. The interaction is similar to selecting an area in an image editor.</p>
<p>We'll first track the starting position of the pointer.</p>
<pre><code class="language-javascript">let isDrawing = false;

let startX = 0;
let startY = 0;

let activeBox = null;
</code></pre>
<p>Listen for the pointer-down event on the redaction layer.</p>
<pre><code class="language-javascript">redactionLayer.addEventListener(
    "pointerdown",
    event =&gt; {

        isDrawing = true;

        const bounds =
            redactionLayer.getBoundingClientRect();

        startX =
            event.clientX - bounds.left;

        startY =
            event.clientY - bounds.top;

        activeBox =
            document.createElement("div");

        activeBox.className =
            "redaction-box";

        activeBox.style.left =
            `${startX}px`;

        activeBox.style.top =
            `${startY}px`;

        redactionLayer.appendChild(activeBox);

    }
);
</code></pre>
<p>As the pointer moves, update the rectangle dimensions.</p>
<pre><code class="language-javascript">redactionLayer.addEventListener(
    "pointermove",
    event =&gt; {

        if (!isDrawing) {
            return;
        }

        const bounds =
            redactionLayer.getBoundingClientRect();

        const currentX =
            event.clientX - bounds.left;

        const currentY =
            event.clientY - bounds.top;

        const width =
            Math.abs(currentX - startX);

        const height =
            Math.abs(currentY - startY);

        activeBox.style.width =
            `${width}px`;

        activeBox.style.height =
            `${height}px`;

        activeBox.style.left =
            `${Math.min(startX, currentX)}px`;

        activeBox.style.top =
            `${Math.min(startY, currentY)}px`;

    }
);
</code></pre>
<p>The <code>Math.min()</code> calls are important because users may drag in any direction. They can begin at the top-left and move down, or start at the bottom-right and drag upward.</p>
<p>When the pointer is released, save the completed rectangle.</p>
<pre><code class="language-javascript">redactionLayer.addEventListener(
    "pointerup",
    () =&gt; {

        if (!isDrawing) {
            return;
        }

        isDrawing = false;

        saveRedaction(activeBox);

        activeBox = null;

    }
);
</code></pre>
<p>The redaction area can be styled as a semi-transparent black rectangle during editing.</p>
<pre><code class="language-css">.redaction-box {
    position: absolute;
    background: rgba(0, 0, 0, 0.8);
    border: 1px dashed #ffffff;
    cursor: move;
}
</code></pre>
<p>Using a transparent preview helps users see which content is currently covered while still recognizing the surrounding page.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/99a4cbb7-d90d-45b0-859b-7e9657e6f30e.png" alt="PDF preview with redaction settings for drawing sensitive areas directly over the document." style="display:block;margin:0 auto" width="644" height="589" loading="lazy">

<h2 id="heading-storing-and-managing-redactions">Storing and Managing Redactions</h2>
<p>Drawing a rectangle visually is only the first step. The application also needs to remember its position and dimensions.</p>
<p>When a redaction box is completed, read its current coordinates.</p>
<pre><code class="language-javascript">function saveRedaction(box) {

    const redaction = {

        x: parseFloat(box.style.left),

        y: parseFloat(box.style.top),

        width: box.offsetWidth,

        height: box.offsetHeight

    };

    if (!redactions[currentPage]) {

        redactions[currentPage] = [];

    }

    redactions[currentPage].push(redaction);

    renderSavedRedactions();

    updateRedactionList();

}
</code></pre>
<p>Because redactions are stored by page, users can navigate through the document without losing their selections.</p>
<p>For example:</p>
<pre><code class="language-javascript">redactions = {

    7: [
        {
            x: 420,
            y: 35,
            width: 310,
            height: 220
        },

        {
            x: 160,
            y: 320,
            width: 150,
            height: 140
        }
    ]

};
</code></pre>
<p>Page 7 now contains two redaction areas.</p>
<p>The interface can display them as <strong>Redaction #1</strong> and <strong>Redaction #2</strong>.</p>
<pre><code class="language-javascript">function updateRedactionList() {

    const list =
        document.getElementById(
            "redactionList"
        );

    list.innerHTML = "";

    const pageRedactions =
        redactions[currentPage] || [];

    pageRedactions.forEach(
        (redaction, index) =&gt; {

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

            item.textContent =
                `Redaction #${index + 1}`;

            list.appendChild(item);

        }
    );

}
</code></pre>
<p>This list gives users a clear overview of the areas selected on the current page.</p>
<h3 id="heading-removing-one-redaction">Removing One Redaction</h3>
<p>Users may accidentally cover the wrong section of a document. They shouldn't have to clear every selection and begin again.</p>
<p>Add a remove button to each redaction item.</p>
<pre><code class="language-javascript">const removeButton =
    document.createElement("button");

removeButton.textContent = "×";

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

        removeRedaction(index);

    }
);

item.appendChild(removeButton);
</code></pre>
<p>Remove only the selected rectangle.</p>
<pre><code class="language-javascript">function removeRedaction(index) {

    redactions[currentPage]
        .splice(index, 1);

    renderSavedRedactions();

    updateRedactionList();

}
</code></pre>
<p>The remaining redactions stay unchanged.</p>
<h3 id="heading-clearing-all-redactions-from-the-current-page">Clearing All Redactions from the Current Page</h3>
<p>The <strong>Clear All on This Page</strong> button removes every selection from the active page.</p>
<pre><code class="language-javascript">clearPageButton.addEventListener(
    "click",
    () =&gt; {

        redactions[currentPage] = [];

        renderSavedRedactions();

        updateRedactionList();

    }
);
</code></pre>
<p>Notice that this doesn't remove redactions created on other pages.</p>
<p>If page 7 is cleared, selections saved on pages 2 or 5 remain available.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/3b42407d-2fa5-4f5b-a268-212671114171.png" alt="Multiple redaction areas displayed on a PDF page with Redaction #1 and Redaction #2 controls, individual remove options, and Clear All on This Page button." style="display:block;margin:0 auto" width="1328" height="713" loading="lazy">

<h2 id="heading-applying-redactions-to-selected-pages">Applying Redactions to Selected Pages</h2>
<p>A redaction tool needs to handle more than a single page.</p>
<p>Sometimes the sensitive information appears only once. In other documents, the same information may repeat across several pages.</p>
<p>For example, a confidential reference number may appear in the header of every page. Manually drawing the same rectangle 50 times would be inefficient.</p>
<p>Our interface provides three page selection modes:</p>
<ol>
<li><p><strong>Current page only</strong> applies the redaction to the active page.</p>
</li>
<li><p><strong>All pages</strong> copies the current page's redaction positions across the complete document.</p>
</li>
<li><p><strong>Specific pages</strong> applies the selections only to page numbers or ranges entered by the user.</p>
</li>
</ol>
<p>Create the page selection controls.</p>
<pre><code class="language-html">&lt;h3&gt;3. 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
    id="pageRange"
    type="text"
    placeholder="e.g., 1, 3-5, 10"&gt;
</code></pre>
<p>Read the selected mode.</p>
<pre><code class="language-javascript">const applyMode =
    document.querySelector(
        'input[name="applyMode"]:checked'
    ).value;
</code></pre>
<p>For the current page, only one page number is required.</p>
<pre><code class="language-javascript">if (applyMode === "current") {

    targetPages = [currentPage];

}
</code></pre>
<p>For all pages, generate the complete page list.</p>
<pre><code class="language-javascript">if (applyMode === "all") {

    targetPages =
        Array.from(
            {
                length:
                    pdfDocument.numPages
            },
            (_, index) =&gt; index + 1
        );

}
</code></pre>
<p>Specific page ranges require a small parser.</p>
<pre><code class="language-javascript">function parsePageRange(value) {

    const pages = new Set();

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

        const range =
            part.trim().split("-");

        if (range.length === 2) {

            const start =
                Number(range[0]);

            const end =
                Number(range[1]);

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

                pages.add(page);

            }

        } else {

            pages.add(Number(range[0]));

        }

    });

    return [...pages];

}
</code></pre>
<p>The value:</p>
<pre><code class="language-text">1, 3-5, 10
</code></pre>
<p>becomes:</p>
<pre><code class="language-javascript">[1, 3, 4, 5, 10]
</code></pre>
<p>Before processing, remove invalid page numbers.</p>
<pre><code class="language-javascript">targetPages =
    targetPages.filter(page =&gt;

        page &gt;= 1 &amp;&amp;

        page &lt;= pdfDocument.numPages

    );
</code></pre>
<p>This page selection feature is particularly useful for repeated headers, footers, document IDs, or other information positioned consistently across several pages.</p>
<h3 id="heading-applying-and-finalizing-the-redactions">Applying and Finalizing the Redactions</h3>
<p>After users have created their redaction areas and selected the target pages, they can click <strong>Apply &amp; Finalize</strong>.</p>
<p>Create the action controls.</p>
<pre><code class="language-html">&lt;div class="action-buttons"&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>Connect the finalize button to the processing function.</p>
<pre><code class="language-javascript">applyButton.addEventListener(
    "click",
    async () =&gt; {

        const pageRedactions =
            redactions[currentPage] || [];

        if (pageRedactions.length === 0) {

            alert(
                "Please add at least one redaction."
            );

            return;

        }

        await generateRedactedPdf();

    }
);
</code></pre>
<p>The <strong>Start Over</strong> button clears the current document and returns users to the upload interface.</p>
<pre><code class="language-javascript">startOverButton.addEventListener(
    "click",
    () =&gt; {

        pdfDocument = null;

        pdfBytes = null;

        currentPage = 1;

        redactions = {};

        pdfInput.value = "";

        location.reload();

    }
);
</code></pre>
<p>In a production application, you can reset individual interface sections instead of reloading the complete page.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/76ffd502-079f-4ea5-84bc-607f94576c72.png" alt="Apply and Finalize button for processing PDF redactions with a Start Over button for uploading a new document." style="display:block;margin:0 auto" width="476" height="67" loading="lazy">

<p>At this stage, users can draw several redaction boxes, remove individual selections, clear all redactions from the active page, and choose exactly which pages should receive the selected redaction areas.</p>
<h2 id="heading-generating-the-redacted-pdf">Generating the Redacted PDF</h2>
<p>The redaction boxes currently exist only in the browser preview. To create the final document, we need to apply those positions to the actual PDF pages.</p>
<p>Load the original PDF using PDF-lib.</p>
<pre><code class="language-javascript">async function generateRedactedPdf() {

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

    const pages =
        pdfDoc.getPages();

    const sourceRedactions =
        redactions[currentPage] || [];

    const targetPages =
        getTargetPages();

}
</code></pre>
<p>Next, loop through the selected pages.</p>
<pre><code class="language-javascript">targetPages.forEach(pageNumber =&gt; {

    const page =
        pages[pageNumber - 1];

    applyPageRedactions(
        page,
        sourceRedactions
    );

});
</code></pre>
<p>The browser preview and PDF page may have different dimensions, so each rectangle must be scaled before it's applied.</p>
<pre><code class="language-javascript">function applyPageRedactions(
    page,
    pageRedactions
) {

    const {
        width: pdfWidth,
        height: pdfHeight
    } = page.getSize();

    const canvas =
        document.getElementById(
            "pdfCanvas"
        );

    const scaleX =
        pdfWidth / canvas.width;

    const scaleY =
        pdfHeight / canvas.height;

    pageRedactions.forEach(
        redaction =&gt; {

            const x =
                redaction.x * scaleX;

            const width =
                redaction.width * scaleX;

            const height =
                redaction.height * scaleY;

            const y =
                pdfHeight -
                (
                    redaction.y +
                    redaction.height
                ) * scaleY;

            page.drawRectangle({

                x,
                y,
                width,
                height,

                color:
                    PDFLib.rgb(0, 0, 0)

            });

        }
    );

}
</code></pre>
<p>Here, the black rectangles are written into the generated PDF page content rather than remaining browser-only preview elements.</p>
<p>Finally, save the processed document.</p>
<pre><code class="language-javascript">const outputBytes =
    await pdfDoc.save();

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

showFinalPreview(outputBlob);
</code></pre>
<p>This distinction is important because the visual result alone should never be used as proof that hidden content has been securely removed.</p>
<h2 id="heading-previewing-and-renaming-the-final-pdf">Previewing and Renaming the Final PDF</h2>
<p>Before downloading the document, users should be able to review the processed pages.</p>
<p>The final preview helps confirm that each selected area appears in the expected position.</p>
<p>Create a new PDF.js document from the processed file.</p>
<pre><code class="language-javascript">async function showFinalPreview(blob) {

    const bytes =
        await blob.arrayBuffer();

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

    renderFinalPage(
        finalPdf,
        1
    );

}
</code></pre>
<p>The preview can use the same page navigation approach we used earlier.</p>
<pre><code class="language-javascript">let finalPage = 1;

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

        if (
            finalPage &gt;=
            finalPdf.numPages
        ) {
            return;
        }

        finalPage++;

        await renderFinalPage(
            finalPdf,
            finalPage
        );

    }
);
</code></pre>
<p>Users can move through the generated PDF and visually check the applied areas before saving the file.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/33bb94bb-cf9b-40af-9c4f-00f09f44aea3.png" alt="Final PDF preview showing black redaction areas applied to selected parts of the document." style="display:block;margin:0 auto" width="933" height="698" loading="lazy">

<p>The tool also allows users to change the output filename.</p>
<p>For example, the automatically generated name might be:</p>
<pre><code class="language-text">combined-images (12)_redacted.pdf
</code></pre>
<p>Create an editable filename input.</p>
<pre><code class="language-html">&lt;input
    id="outputFilename"
    type="text"
    value="document_redacted.pdf"&gt;
</code></pre>
<p>Before downloading, make sure the filename ends with <code>.pdf</code>.</p>
<pre><code class="language-javascript">function getOutputFilename() {

    let filename =
        outputFilename.value.trim();

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

        filename += ".pdf";

    }

    return filename;

}
</code></pre>
<p>Allowing the filename to be changed is useful when users process several versions of the same document.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/33d31497-de87-4a04-8792-1dbc9fab8219.png" alt="Editable filename field for renaming the redacted PDF before download." style="display:block;margin:0 auto" width="421" height="162" loading="lazy">

<h2 id="heading-downloading-the-final-pdf">Downloading the Final PDF</h2>
<p>The final result section displays basic information about the processed document.</p>
<p>In this project, users can see the filename, total number of pages, and final file size before downloading.</p>
<p>Calculate the file size in megabytes.</p>
<pre><code class="language-javascript">function formatFileSize(bytes) {

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

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

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

        const url =
            URL.createObjectURL(
                outputBlob
            );

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

        link.href = url;

        link.download =
            getOutputFilename();

        link.click();

        URL.revokeObjectURL(url);

    }
);
</code></pre>
<p>The browser downloads the generated PDF using the filename selected by the user.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/2cc1b5e6-123a-4e8a-a669-94b1440e4cb9.png" alt="Redacted PDF download section showing filename, total pages, file size, and Download button" style="display:block;margin:0 auto" width="427" height="371" loading="lazy">

<p>.After downloading, users can click <strong>Start Over</strong> to clear the existing document and process another PDF.</p>
<pre><code class="language-javascript">function resetTool() {

    pdfDocument = null;

    pdfBytes = null;

    redactions = {};

    currentPage = 1;

    pdfInput.value = "";

    showUploadSection();

}
</code></pre>
<p>The complete processing workflow is now connected: users can mark areas on a page, apply those selections, preview the result, rename the generated file, review its details, and download the processed PDF.</p>
<h2 id="heading-demo-how-the-pdf-redaction-tool-works">Demo: How the PDF Redaction Tool Works</h2>
<p>Now that the complete redaction workflow is connected, let's walk through the application from upload to download.</p>
<h3 id="heading-step-1-upload-the-pdf-document">Step 1: Upload the PDF Document</h3>
<p>Users begin by uploading a PDF through the drag-and-drop area or by clicking the <strong>Select PDF</strong> button.</p>
<p>The browser validates the file, reads the document into memory, and prepares it for local processing. No backend server is required for this workflow.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/fa997ccc-4de4-40a8-bb8b-051798b408d3.png" alt="PDF Redaction Tool upload area with drag-and-drop support and Select PDF button." style="display:block;margin:0 auto" width="640" height="654" loading="lazy">

<h3 id="heading-step-2-preview-and-navigate-the-pdf">Step 2: Preview and Navigate the PDF</h3>
<p>After the file loads, the current PDF page appears inside the preview area.</p>
<p>Page navigation controls allow users to move backward and forward through the document. The current page number and total page count are displayed below the preview.</p>
<p>Users can review the document first and navigate to the page containing the information they want to cover.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/f86bd6e4-88bf-4716-9274-e1d7f1104fae.png" alt="Uploaded PDF preview with current page number and previous and next page navigation controls." style="display:block;margin:0 auto" width="642" height="539" loading="lazy">

<h3 id="heading-step-3-configure-the-redaction">Step 3: Configure the Redaction</h3>
<p>The redaction settings appear beside the PDF preview.</p>
<p>Users draw a rectangle directly over the document by clicking and dragging across the sensitive area. The selected region appears as a dark overlay, making the chosen position easy to verify.</p>
<p>The page application controls also let users choose whether the same redaction position should be applied to the current page, all pages, or specific page numbers.</p>
<p>This is helpful when the same field appears in a consistent position across several pages.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/72609da4-75b1-48cc-a97f-31a00e8f2843.png" alt="Alt Text: PDF redaction settings with a selected redaction area and page application controls." style="display:block;margin:0 auto" width="644" height="589" loading="lazy">

<h3 id="heading-step-4-manage-multiple-redaction-areas">Step 4: Manage Multiple Redaction Areas</h3>
<p>A single page may contain several areas that need to be covered.</p>
<p>For example, users may select a name near the top of the page and another piece of information farther down the document.</p>
<p>Each selection appears separately as <strong>Redaction #1</strong>, <strong>Redaction #2</strong>, and so on.</p>
<p>Individual remove controls allow users to delete one selection without affecting the others. The <strong>Clear All on This Page</strong> option removes every redaction area from the active page.</p>
<p>This gives users a chance to correct selections before processing the PDF.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/7f621f55-ae1b-419d-afdd-5e73b4d8e967.png" alt="Multiple PDF redaction areas with individual remove controls and Clear All on This Page option." style="display:block;margin:0 auto" width="1328" height="713" loading="lazy">

<h3 id="heading-step-5-apply-and-finalize-the-pdf">Step 5: Apply and Finalize the PDF</h3>
<p>Once the redaction areas and target pages are ready, users click <strong>Apply &amp; Finalize</strong>.</p>
<p>The application converts the browser selection coordinates into PDF page coordinates and applies the opaque areas to the selected pages while generating the processed document.</p>
<p>If the wrong PDF was uploaded or users want to begin again, the <strong>Start Over</strong> button resets the current workflow.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/e0f1469e-f24f-4297-bb15-12ba8097bc8e.png" alt="Apply and Finalize button with Start Over option in the PDF Redaction Tool." style="display:block;margin:0 auto" width="476" height="67" loading="lazy">

<h3 id="heading-step-6-preview-the-processed-pdf">Step 6: Preview the Processed PDF</h3>
<p>After processing finishes, the generated PDF appears in a new preview section.</p>
<p>Users can navigate through the document and visually confirm that the selected areas are covered in the expected locations.</p>
<p>This review stage is important. A small coordinate or page-selection mistake could leave information visible on another page.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/e86098b0-5352-438e-8b02-2346e9f12d72.png" alt="Processed PDF preview showing opaque redaction areas applied to the document." style="display:block;margin:0 auto" width="933" height="698" loading="lazy">

<h3 id="heading-step-7-rename-the-pdf">Step 7: Rename the PDF</h3>
<p>Before downloading, users can edit the generated filename.</p>
<p>The application may automatically add <code>_redacted</code> to the original filename, but users can replace it with a name that better matches their document workflow.</p>
<p>For example:</p>
<pre><code class="language-text">customer-record_redacted.pdf
</code></pre>
<p>or:</p>
<pre><code class="language-text">public-report-copy.pdf
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/75b01689-564d-40a0-94eb-3b69759af445.png" alt="Filename editing option for renaming the processed PDF before download." style="display:block;margin:0 auto" width="421" height="162" loading="lazy">

<h3 id="heading-step-8-review-file-details-and-download">Step 8: Review File Details and Download</h3>
<p>The final section displays the output filename, number of pages, and generated file size.</p>
<p>After reviewing these details, users click <strong>Download</strong> to save the PDF locally.</p>
<p>The browser creates the download directly from the processed document data.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/f04502fb-9294-40d3-90d8-46d45aa128fe.png" alt="PDF download section showing output filename, total pages, file size, and Download button." style="display:block;margin:0 auto" width="427" height="371" loading="lazy">

<h3 id="heading-step-9-start-over-with-another-pdf">Step 9: Start Over with Another PDF</h3>
<p>After downloading the file, users can click <strong>Start Over</strong>.</p>
<p>The application clears the current PDF, page preview, stored coordinates, and generated result before returning to the upload interface.</p>
<p>Users can then process another document without manually refreshing the browser.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/dbc3e01a-ccde-44a3-a5a5-f61111bdec2b.png" alt="Start Over button for clearing the current redaction session and uploading another PDF." style="display:block;margin:0 auto" width="200" height="78" loading="lazy">

<h2 id="heading-how-to-verify-the-redacted-pdf">How to Verify the Redacted PDF</h2>
<p>A document that looks correct in the preview should still be checked before it's shared.</p>
<p>First, navigate through every affected page and confirm that each intended area is fully covered. Pay particular attention to redactions applied across multiple pages because page layouts may not always be identical.</p>
<p>The application can perform a simple check to confirm that redaction areas exist before finalization.</p>
<pre><code class="language-javascript">const totalRedactions =
    Object.values(redactions)
        .reduce(
            (total, items) =&gt;
                total + items.length,
            0
        );

if (totalRedactions === 0) {

    alert(
        "No redaction areas were added."
    );

    return;

}
</code></pre>
<p>You should also verify that every selected target page exists.</p>
<pre><code class="language-javascript">const validPages =
    targetPages.every(page =&gt;

        page &gt;= 1 &amp;&amp;

        page &lt;= pdfDocument.numPages

    );
</code></pre>
<p>For visual masking workflows like the rectangle-based implementation shown here, remember that an opaque rectangle does <strong>not by itself prove that the underlying PDF content has been securely removed</strong>.</p>
<p>If the document contains legally protected, highly confidential, or compliance-sensitive information, use a standards-based redaction and sanitization process that removes the underlying content objects, then test the result for text selection, searchability, annotations, metadata, and other recoverable content before sharing it.</p>
<p>That verification distinction is especially important in a redaction project.</p>
<h2 id="heading-performance-optimization-tips">Performance Optimization Tips</h2>
<p>PDF redaction itself may appear simple, but page rendering and document generation can consume noticeable browser memory when working with large files.</p>
<p>Avoid rendering every PDF page at full resolution simultaneously. Render the active page when users navigate to it.</p>
<pre><code class="language-javascript">await renderPage(currentPage);
</code></pre>
<p>Store redaction coordinates as small JavaScript objects rather than saving complete canvas images.</p>
<pre><code class="language-javascript">redactions[currentPage].push({

    x,
    y,
    width,
    height

});
</code></pre>
<p>When generating the output, modify only the required pages.</p>
<pre><code class="language-javascript">for (
    const pageNumber of targetPages
) {

    const page =
        pages[pageNumber - 1];

    applyPageRedactions(
        page,
        sourceRedactions
    );

}
</code></pre>
<p>Temporary object URLs should also be released after use.</p>
<pre><code class="language-javascript">URL.revokeObjectURL(url);
</code></pre>
<p>These choices keep the interface responsive and reduce unnecessary memory use, particularly when processing long reports or multi-page business documents.</p>
<h2 id="heading-important-notes-and-common-mistakes">Important Notes and Common Mistakes</h2>
<p>The most common mistake in a redaction interface is incorrect coordinate conversion.</p>
<p>The canvas preview and actual PDF page may have different dimensions. Applying browser coordinates directly to the PDF can move the rectangle away from the intended content.</p>
<p>Always calculate the scale values first.</p>
<pre><code class="language-javascript">const scaleX =
    pdfWidth / canvas.width;

const scaleY =
    pdfHeight / canvas.height;
</code></pre>
<p>Another mistake is forgetting that PDF and canvas Y coordinates may use different origins.</p>
<pre><code class="language-javascript">const pdfY =
    pdfHeight -
    (
        redaction.y +
        redaction.height
    ) * scaleY;
</code></pre>
<p>Users should also be careful when applying one selection to all pages. A header may appear in the same position throughout a document, but other page layouts can change. Always preview the processed result.</p>
<p>Validate custom page ranges before processing.</p>
<pre><code class="language-javascript">targetPages =
    targetPages.filter(page =&gt;

        Number.isInteger(page) &amp;&amp;

        page &gt;= 1 &amp;&amp;

        page &lt;= pdfDocument.numPages

    );
</code></pre>
<p>Finally, don't describe a visually covered area as securely removed unless the implementation actually removes the underlying text, image, annotation, and related content from the PDF structure.</p>
<p>For a simple browser project, opaque masking demonstrates coordinate mapping and PDF modification well. A production redaction system handling sensitive information requires stronger content-removal and output-sanitization logic.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this tutorial, you built a browser-based PDF redaction interface using JavaScript.</p>
<p>You learned how to upload a PDF, render document pages with PDF.js, navigate between pages, draw redaction areas, store rectangle coordinates, manage multiple selections, choose target pages, convert canvas coordinates into PDF coordinates, generate a processed PDF, preview the result, rename the output file, and download it locally.</p>
<p>You also saw an important security distinction: visually covering content with an opaque rectangle is not automatically the same as permanently removing the underlying PDF content. That difference matters when moving from a learning project to a production-grade redaction system.</p>
<p>You can explore the browser-based workflow with the <a href="https://allinonetools.net/redact-pdf/">PDF Redaction Tool.</a></p>
<p>Once you understand the coordinate and page-processing workflow, you can extend the project with searchable-text detection, automatic pattern identification, annotation cleanup, metadata sanitization, redaction verification, or a true content-removal pipeline.</p>
<p>The same coordinate-mapping concepts can also be reused when building PDF annotation, signature, highlighting, cropping, and document review tools.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Intro to Shaders: JavaScript and p5.js Course for Beginners ]]>
                </title>
                <description>
                    <![CDATA[ Are you ready to unlock the true rendering power of your computer and create breathtaking visuals? We just published a new course on the freeCodeCamp.org YouTube channel that will teach you the fundam ]]>
                </description>
                <link>https://www.freecodecamp.org/news/intro-to-shaders-javascript-and-p5-js-course-for-beginners/</link>
                <guid isPermaLink="false">6a58c1f600ad718fe90bfe7d</guid>
                
                    <category>
                        <![CDATA[ p5.js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ youtube ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Beau Carnes ]]>
                </dc:creator>
                <pubDate>Thu, 16 Jul 2026 11:35:18 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5f68e7df6dfc523d0a894e7c/bfb3c226-4906-49bb-9a6b-c75652ec147e.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Are you ready to unlock the true rendering power of your computer and create breathtaking visuals? We just published a new course on the <a href="http://freeCodeCamp.org">freeCodeCamp.org</a> YouTube channel that will teach you the fundamentals of graphics programming.</p>
<p>This beginner-friendly course, developed by Patt Vira, is designed for anyone eager to break into WebGL and the math of motion, requiring absolutely zero prior shader experience.</p>
<p>In this course, you will learn exactly why shaders are so much faster than traditional CPU rendering by understanding the difference between how CPUs and GPUs handle data. You will dive into the GLSL language to write code that executes on millions of pixels simultaneously. You will progressively master core concepts like mapping coordinate spaces, leveraging shaping functions to sculpt colors, domain repetition for tiling, and using distance functions to draw and animate complex shapes.</p>
<p>By following along using JavaScript and the p5.js library, you will apply these exact techniques to build a stunning, animated, glowing fractal heart entirely from scratch. You will learn how to structure your files, pass dynamic variables from your JavaScript code to the GPU, and mix colors using cosine-based palettes for a rich, professional spectrum.</p>
<p>You can watch the <a href="https://youtu.be/YdhXnB5E-4s">full course on the YouTube channel</a> (1-hour watch).</p>
<div class="embed-wrapper"><iframe width="560" height="315" src="https://www.youtube.com/embed/YdhXnB5E-4s" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build a RAG Chatbot for Your Docs with Node.js, Google Gemini, and pgvector ]]>
                </title>
                <description>
                    <![CDATA[ I was helping a team that had a 200-page API documentation PDF. Every new engineer spent their first two weeks Ctrl+F-ing through it, asking the same questions in Slack, getting redirected to the same ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-rag-chatbot-nodejs-gemini-pgvector/</link>
                <guid isPermaLink="false">6a57a6aa328507d0d4d46169</guid>
                
                    <category>
                        <![CDATA[ Node.js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ PostgreSQL ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Tutorial ]]>
                    </category>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ RAG  ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Zia Ullah ]]>
                </dc:creator>
                <pubDate>Wed, 15 Jul 2026 15:26:34 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/9aa3d8d3-9c51-42a7-8e78-907802394ea1.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>I was helping a team that had a 200-page API documentation PDF. Every new engineer spent their first two weeks Ctrl+F-ing through it, asking the same questions in Slack, getting redirected to the same paragraphs on page 47.</p>
<p>The doc was accurate. It was even well-written. But nobody could find anything in it fast enough for it to be useful.</p>
<p>That's the problem RAG, or Retrieval-Augmented Generation, solves.</p>
<p>The naïve approach is to stuff your entire PDF into a prompt and let the model figure it out. That breaks down fast: context windows overflow, costs spike on every request, and the model loses the thread somewhere in the wall of text.</p>
<p>RAG takes a different approach. Your documents get broken into small chunks upfront. Ask it a question and it digs out the 3 or 4 chunks that best match it — those are what the model actually sees. The model gets a tight, focused context. The answer comes from what your document actually says — not from whatever the LLM memorized during training.</p>
<p>In this tutorial, you'll build that from scratch. Upload any PDF — an API reference, an internal spec, a research paper — and ask questions about it in plain English. The system finds the relevant sections and answers from the document itself, not from general training data.</p>
<p>The stack: Node.js with Express, Google Gemini for embeddings, Groq for text generation, and pgvector running in Docker. Every piece of it is free — no credit card, no trial period.</p>
<p>The complete code is on GitHub at <a href="https://github.com/ziaongit/nodejs-rag-chatbot">nodejs-rag-chatbot</a>.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-how-rag-works">How RAG Works</a></p>
</li>
<li><p><a href="#heading-what-were-building">What We're Building</a></p>
</li>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-project-setup">Project Setup</a></p>
</li>
<li><p><a href="#heading-set-up-postgres-with-pgvector-using-docker">Set Up Postgres with pgvector Using Docker</a></p>
</li>
<li><p><a href="#heading-connect-to-the-database">Connect to the Database</a></p>
</li>
<li><p><a href="#heading-build-the-ingestion-pipeline">Build the Ingestion Pipeline</a></p>
</li>
<li><p><a href="#heading-build-the-query-pipeline">Build the Query Pipeline</a></p>
</li>
<li><p><a href="#heading-build-the-chat-api-with-express">Build the Chat API with Express</a></p>
</li>
<li><p><a href="#heading-test-the-chatbot">Test the Chatbot</a></p>
</li>
<li><p><a href="#heading-troubleshooting">Troubleshooting</a></p>
</li>
<li><p><a href="#heading-how-to-swap-in-openai">How to Swap in OpenAI</a></p>
</li>
<li><p><a href="#heading-what-to-build-next">What to Build Next</a></p>
</li>
</ul>
<h2 id="heading-how-rag-works">How RAG Works</h2>
<p>RAG has two phases, and the code maps directly to both.</p>
<p><strong>Ingestion phase</strong> — runs once when you upload a document:</p>
<ol>
<li><p>Pull the raw text out of the PDF</p>
</li>
<li><p>Break it into chunks of 400 to 600 characters each, with a bit of overlap so nothing important gets cut at a boundary</p>
</li>
<li><p>Run each chunk through an embedding model, which turns it into a vector (a long list of numbers that captures what the text means)</p>
</li>
<li><p>Store each chunk and its vector in Postgres</p>
</li>
</ol>
<p><strong>Query phase</strong> — runs every time someone asks a question:</p>
<ol>
<li><p>Embed the user's question using the same model</p>
</li>
<li><p>Search the database for chunks whose vectors are closest to the question vector</p>
</li>
<li><p>Take the top 5 matching chunks and assemble them into a context block</p>
</li>
<li><p>Send <code>context + question</code> to the LLM and return its answer</p>
</li>
</ol>
<p>The reason this works better than keyword search: the embedding model captures <em>meaning</em>, not just exact words. If your doc says "terminate the process" and the user asks "how do I stop it?", vector similarity finds that match. Regular string matching doesn't.</p>
<p>One thing that trips people up: you must use the same embedding model at query time as you did at ingestion. The model defines the geometric space those vectors live in. Switch models halfway through and the coordinates stop meaning the same thing — you'd be comparing apples to completely different apples.</p>
<h2 id="heading-what-were-building">What We're Building</h2>
<p>The architecture is intentionally minimal: two endpoints, with nothing you don't need:</p>
<ul>
<li><p><code>POST /ingest</code>: accepts a PDF upload, chunks it, embeds each chunk, stores vectors in pgvector</p>
</li>
<li><p><code>POST /chat</code>: accepts a question, retrieves the most relevant chunks, returns an LLM-generated answer</p>
</li>
</ul>
<p>The full tech stack:</p>
<ul>
<li><p><strong>Node.js + Express</strong> — API layer</p>
</li>
<li><p><strong>Google Gemini free API</strong> — <code>gemini-embedding-001</code> for embeddings (3,072 dimensions per chunk)</p>
</li>
<li><p><strong>Groq free API</strong> — <code>llama-3.1-8b-instant</code> for text generation</p>
</li>
<li><p><strong>PostgreSQL + pgvector</strong> — vector storage and cosine similarity search, running in Docker</p>
</li>
<li><p><strong>pdf-parse</strong> — extracts raw text from PDF buffers</p>
</li>
</ul>
<p>Gemini handles embeddings and Groq handles generation. Splitting them across two providers isn't arbitrary. Gemini's generation API has a quota limit of zero in certain regions (including Pakistan), while Groq works everywhere with no restrictions. Using Groq for generation means this tutorial runs the same way regardless of where you are.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before you start:</p>
<ul>
<li><p>Node.js 20+ installed on your machine</p>
</li>
<li><p>Docker Desktop running (this is how we'll run Postgres locally)</p>
</li>
<li><p>A free Google Gemini API key (for embeddings)</p>
</li>
<li><p>A free Groq API key (for text generation)</p>
</li>
</ul>
<h3 id="heading-how-to-get-your-free-gemini-api-key">How to Get Your Free Gemini API Key</h3>
<ol>
<li><p>Go to <a href="https://aistudio.google.com/app/apikey">aistudio.google.com/app/apikey</a> and sign in with a Google account</p>
</li>
<li><p>Click "Create API key"</p>
</li>
<li><p>Select "Create API key in new project"</p>
</li>
<li><p>Copy the key — it starts with <code>AIzaSy...</code></p>
</li>
</ol>
<p>No credit card or billing required.</p>
<h3 id="heading-how-to-get-your-free-groq-api-key">How to Get Your Free Groq API Key</h3>
<ol>
<li><p>Go to <a href="https://console.groq.com">console.groq.com</a> and sign up with Google</p>
</li>
<li><p>Click "API Keys" in the left sidebar</p>
</li>
<li><p>Click "Create API Key", give it a name, copy the key — it starts with <code>gsk_...</code></p>
</li>
</ol>
<p>Groq is free with generous rate limits and works in all regions.</p>
<h2 id="heading-project-setup">Project Setup</h2>
<p>Create the project directory and initialize it:</p>
<pre><code class="language-bash">mkdir nodejs-rag-chatbot
cd nodejs-rag-chatbot
npm init -y
</code></pre>
<p>Install dependencies:</p>
<pre><code class="language-bash">npm install express pg pdf-parse uuid dotenv multer
npm install --save-dev nodemon
</code></pre>
<p>A quick note on the packages: <code>multer</code> is what makes file uploads work on the <code>/ingest</code> endpoint. Without it, Express can't parse multipart form data.</p>
<p><code>pdf-parse</code> does the heavy lifting on PDFs, though watch out for scanned PDFs. Those are just images with no text layer underneath, so you'll get back an empty string.</p>
<p><code>pg</code> talks to Postgres, <code>uuid</code> gives each row a unique ID, and <code>dotenv</code> loads your keys before the app does anything.</p>
<p>Create a <code>.env</code> in the project root. It needs seven values:</p>
<pre><code class="language-plaintext">GEMINI_API_KEY=AIzaSy...         ← your Gemini key from Google AI Studio
GROQ_API_KEY=gsk_...             ← your Groq key from console.groq.com
POSTGRES_USER=rag_user
POSTGRES_PASSWORD=rag_pass       ← choose any password, this is local only
POSTGRES_DB=rag_db
DATABASE_URL=postgresql://rag_user:rag_pass@localhost:5432/rag_db
PORT=3000
</code></pre>
<p>One thing: the password in <code>POSTGRES_PASSWORD</code> and the one in <code>DATABASE_URL</code> must match exactly. I changed just one of them once and spent way too long debugging a "password authentication failed" error before realising the two values were out of sync.</p>
<p>Update <code>package.json</code> scripts:</p>
<pre><code class="language-json">"scripts": {
  "start": "node src/index.js",
  "dev": "nodemon src/index.js"
}
</code></pre>
<p>Create the <code>src</code> directory:</p>
<pre><code class="language-bash">mkdir src
</code></pre>
<p>Your final folder structure will look like this:</p>
<pre><code class="language-plaintext">nodejs-rag-chatbot/
├── src/
│   ├── index.js        ← Express app entry point
│   ├── db.js           ← Postgres connection and schema setup
│   ├── embeddings.js   ← Gemini embedding + Groq generation
│   ├── ingest.js       ← Document ingestion pipeline
│   └── query.js        ← RAG query pipeline
├── docker-compose.yml
├── .env
└── package.json
</code></pre>
<h2 id="heading-set-up-postgres-with-pgvector-using-docker">Set Up Postgres with pgvector Using Docker</h2>
<p>pgvector adds a <code>vector</code> column type to Postgres and the operators needed to search it by similarity. Normally you'd have to install it yourself, but the <code>pgvector/pgvector</code> Docker image ships with it already baked in. Just pull the image and you're good.</p>
<p>Now add <code>docker-compose.yml</code> to the project root:</p>
<pre><code class="language-yaml">services:
  postgres:
    image: pgvector/pgvector:pg16
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
    ports:
      - "5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:
</code></pre>
<p>Those <code>${VARIABLE}</code> references get swapped out from <code>.env</code> when Compose starts — so <code>docker-compose.yml</code> itself stays clean. This is worth doing from day one. I've seen people skip this and regret it after a repo goes public.</p>
<p>Start it:</p>
<pre><code class="language-bash">docker compose up -d
</code></pre>
<h2 id="heading-connect-to-the-database">Connect to the Database</h2>
<p>Create <code>src/db.js</code>. This sets up the connection pool and creates the <code>documents</code> table on first run:</p>
<pre><code class="language-javascript">const { Pool } = require('pg');

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
});

async function initDb() {
  await pool.query(`CREATE EXTENSION IF NOT EXISTS vector`);

  await pool.query(`
    CREATE TABLE IF NOT EXISTS documents (
      id UUID PRIMARY KEY,
      content TEXT NOT NULL,
      source TEXT NOT NULL,
      embedding VECTOR(3072)
    )
  `);

  console.log('Database ready');
}

module.exports = { pool, initDb };
</code></pre>
<p>The <code>VECTOR(3072)</code> dimension matches the output of Gemini's <code>gemini-embedding-001</code> model exactly. If you use a different embedding model in the future, check its output dimensions and update this number to match.</p>
<h2 id="heading-build-the-ingestion-pipeline">Build the Ingestion Pipeline</h2>
<p>Start with <code>embeddings.js</code>. This file is the bridge to both external APIs — Gemini for turning text into vectors, Groq for generating the final answer. Keeping both in one place means a single file to touch if you ever swap providers.</p>
<p><strong>src/embeddings.js:</strong></p>
<pre><code class="language-javascript">const GEMINI_KEY = process.env.GEMINI_API_KEY;
const GEMINI_BASE = 'https://generativelanguage.googleapis.com/v1/models';

async function embedText(text) {
  const res = await fetch(
    `${GEMINI_BASE}/gemini-embedding-001:embedContent?key=${GEMINI_KEY}`,
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ content: { parts: [{ text }] } }),
    }
  );
  const data = await res.json();
  if (!res.ok) throw new Error(JSON.stringify(data));
  return data.embedding.values;
}

async function generateAnswer(context, question) {
  const res = await fetch(
    'https://api.groq.com/openai/v1/chat/completions',
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${process.env.GROQ_API_KEY}`,
      },
      body: JSON.stringify({
        model: 'llama-3.1-8b-instant',
        messages: [
          {
            role: 'system',
            content: 'You are a helpful assistant. Answer the question using only the context provided. If the context does not contain enough information, say so clearly.',
          },
          {
            role: 'user',
            content: `Context:\n${context}\n\nQuestion: ${question}`,
          },
        ],
      }),
    }
  );
  const data = await res.json();
  if (!res.ok) throw new Error(JSON.stringify(data));
  return data.choices[0].message.content;
}

module.exports = { embedText, generateAnswer };
</code></pre>
<p>We're calling both APIs directly with Node.js's built-in <code>fetch</code> rather than the official SDKs. The reason is practical: Google's Node.js SDK routes requests through the <code>v1beta</code> endpoint by default, and <code>gemini-embedding-001</code> isn't available there — only on <code>v1</code>. Direct fetch sidesteps that entirely and keeps the dependency count low.</p>
<p><strong>src/ingest.js:</strong></p>
<pre><code class="language-javascript">const pdfParse = require('pdf-parse');
const { v4: uuidv4 } = require('uuid');
const { pool } = require('./db');
const { embedText } = require('./embeddings');

function chunkText(text, chunkSize = 500, overlap = 50) {
  const chunks = [];
  let start = 0;

  while (start &lt; text.length) {
    const end = Math.min(start + chunkSize, text.length);
    chunks.push(text.slice(start, end).trim());
    start += chunkSize - overlap;
  }

  return chunks.filter(chunk =&gt; chunk.length &gt; 50);
}

async function ingestDocument(buffer, filename) {
  const { text } = await pdfParse(buffer);
  const chunks = chunkText(text);

  console.log(`Processing ${chunks.length} chunks from "${filename}"`);

  for (const chunk of chunks) {
    const embedding = await embedText(chunk);

    await pool.query(
      `INSERT INTO documents (id, content, source, embedding)
       VALUES ($1, $2, $3, $4::vector)`,
      [uuidv4(), chunk, filename, JSON.stringify(embedding)]
    );
  }

  return chunks.length;
}

module.exports = { ingestDocument };
</code></pre>
<p>500 characters per chunk, with 50 characters of overlap between neighbours.</p>
<p>Why the overlap? Without it, a sentence that straddles a boundary gets split, half in one chunk, half in the next — and neither piece makes sense on its own when retrieved. The overlap keeps those boundary sentences intact.</p>
<p>For most technical docs, 500 is a good starting point. Dense legal or financial text tends to need something closer to 300.</p>
<h2 id="heading-build-the-query-pipeline">Build the Query Pipeline</h2>
<p><strong>src/query.js:</strong></p>
<pre><code class="language-javascript">const { pool } = require('./db');
const { embedText, generateAnswer } = require('./embeddings');

async function queryDocuments(question) {
  const questionEmbedding = await embedText(question);

  const { rows } = await pool.query(
    `SELECT content, source,
            1 - (embedding &lt;=&gt; $1::vector) AS similarity
     FROM documents
     ORDER BY embedding &lt;=&gt; $1::vector
     LIMIT 5`,
    [JSON.stringify(questionEmbedding)]
  );

  if (rows.length === 0) {
    return { answer: 'No relevant documents found.', sources: [] };
  }

  const context = rows.map(r =&gt; r.content).join('\n\n---\n\n');
  const answer = await generateAnswer(context, question);

  return {
    answer,
    sources: [...new Set(rows.map(r =&gt; r.source))],
    topSimilarity: parseFloat(rows[0].similarity).toFixed(3),
  };
}

module.exports = { queryDocuments };
</code></pre>
<p>The <code>&lt;=&gt;</code> operator is pgvector's cosine distance. Semantically similar text produces vectors that point in the same direction — so the distance between them is small. Flip that with <code>1 - distance</code> and you get a similarity score, where anything close to 1 means a strong match.</p>
<p>I found 0.7 to be a reliable threshold in my testing — chunks above that were almost always relevant. Anything below 0.5 and the retrieval was really stretching, pulling chunks that shared a keyword or two but weren't actually answering the question.</p>
<p>When that happens, the system prompt instruction ("if the context does not contain enough information, say so clearly") becomes important. A well-behaved model will tell the user it doesn't know rather than guess.</p>
<p>We also surface the source filename. Once you've ingested more than one document, users need to know whether that answer came from the architecture spec or the incident report.</p>
<h2 id="heading-build-the-chat-api-with-express">Build the Chat API with Express</h2>
<p><strong>src/index.js:</strong></p>
<pre><code class="language-javascript">require('dotenv').config();
const express = require('express');
const multer = require('multer');
const { initDb } = require('./db');
const { ingestDocument } = require('./ingest');
const { queryDocuments } = require('./query');

const app = express();
const upload = multer({ storage: multer.memoryStorage() });

app.use(express.json());

app.post('/ingest', upload.single('file'), async (req, res) =&gt; {
  if (!req.file) {
    return res.status(400).json({ error: 'No file uploaded' });
  }

  if (!req.file.mimetype.includes('pdf')) {
    return res.status(400).json({ error: 'Only PDF files are supported' });
  }

  try {
    const count = await ingestDocument(req.file.buffer, req.file.originalname);
    res.json({ message: `Ingested ${count} chunks from "${req.file.originalname}"` });
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: 'Ingestion failed', detail: err.message });
  }
});

app.post('/chat', async (req, res) =&gt; {
  const { question } = req.body;

  if (!question || typeof question !== 'string') {
    return res.status(400).json({ error: 'question is required' });
  }

  try {
    const result = await queryDocuments(question);
    res.json(result);
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: 'Query failed', detail: err.message });
  }
});

const PORT = process.env.PORT || 3000;

initDb().then(() =&gt; {
  app.listen(PORT, () =&gt; {
    console.log(`RAG chatbot running on port ${PORT}`);
  });
});
</code></pre>
<p><code>memoryStorage()</code> keeps the uploaded file in a buffer instead of writing it to disk. We parse it and store the chunks immediately, so there's nothing to save.</p>
<h2 id="heading-test-the-chatbot">Test the Chatbot</h2>
<p>Start the server:</p>
<pre><code class="language-bash">npm run dev
</code></pre>
<p>You should see:</p>
<pre><code class="language-plaintext">Database ready
RAG chatbot running on port 3000
</code></pre>
<p>Upload a PDF. Any PDF works. I tested with a copy of a Node.js best practices guide:</p>
<pre><code class="language-bash"># Linux / macOS
curl -X POST http://localhost:3000/ingest -F "file=@your-document.pdf"

# Windows PowerShell
curl.exe -X POST http://localhost:3000/ingest -F "file=@your-document.pdf"
</code></pre>
<p>Response:</p>
<pre><code class="language-json">{ "message": "Ingested 142 chunks from \"your-document.pdf\"" }
</code></pre>
<p>Now ask a question:</p>
<pre><code class="language-bash"># Linux / macOS
curl -X POST http://localhost:3000/chat \
  -H "Content-Type: application/json" \
  -d '{ "question": "How should I handle errors in async functions?" }'

# Windows PowerShell
curl.exe -X POST http://localhost:3000/chat -H "Content-Type: application/json" -d "{\"question\": \"How should I handle errors in async functions?\"}"
</code></pre>
<p>Response:</p>
<pre><code class="language-json">{
  "answer": "For async functions in Node.js, wrap your logic in a try/catch block to handle rejected promises. In Express, pass the caught error to next(err) to trigger your error-handling middleware. Alternatively, you can create a wrapper function that wraps any async route handler in a promise and calls next on rejection, keeping your route handlers clean...",
  "sources": ["your-document.pdf"],
  "topSimilarity": "0.841"
}
</code></pre>
<p>The <code>topSimilarity</code> score tells you how well the retrieval went. Above 0.7 and the chunks pulled were genuinely relevant. Below 0.5, and the search was struggling: it found something, but not something that actually answers the question.</p>
<p>Try asking about something your PDF doesn't mention. If the system prompt is doing its job, the model should say it doesn't have enough information rather than making something up. That's the behaviour you want in production.</p>
<p>The repo includes two diagnostic scripts that are useful if anything isn't working:</p>
<ul>
<li><p><code>node test-keys.js</code> — tests both API keys live and reports whether each one succeeds</p>
</li>
<li><p><code>node list-models.js</code> — fetches the full list of Gemini models available to your API key</p>
</li>
</ul>
<p>Run these before diving into the troubleshooting section below.</p>
<h2 id="heading-troubleshooting">Troubleshooting</h2>
<p>Everything in this section is a real error I hit while building this. Nothing hypothetical.</p>
<h3 id="heading-port-5432-is-already-in-use">Port 5432 is already in use</h3>
<pre><code class="language-plaintext">Error: bind: address already in use
</code></pre>
<p>Something else — probably a local Postgres install — is already on that port. Two fixes are needed. First, in <code>docker-compose.yml</code>:</p>
<pre><code class="language-yaml">ports:
  - "5433:5432"
</code></pre>
<p>Second, update <code>DATABASE_URL</code> in <code>.env</code>:</p>
<pre><code class="language-plaintext">DATABASE_URL=postgresql://rag_user:rag_pass@localhost:5433/rag_db
</code></pre>
<p>The container itself still listens on 5432 internally. You're just changing which port your machine uses to reach it.</p>
<h3 id="heading-password-authentication-failed-for-user-raguser">Password authentication failed for user "rag_user"</h3>
<pre><code class="language-plaintext">Error: password authentication failed for user "rag_user"
</code></pre>
<p>The password Postgres was initialized with doesn't match what your app is sending. Open <code>.env</code> and compare <code>POSTGRES_PASSWORD</code> with the password embedded in <code>DATABASE_URL</code>. They need to be character-for-character identical.</p>
<p>After fixing the mismatch, the old volume still has the wrong password baked into it. You must destroy it and start fresh:</p>
<pre><code class="language-bash">docker compose down -v
docker compose up -d
</code></pre>
<p>The <code>-v</code> flag deletes the data volume. Postgres reinitializes on the next start with the credentials from your current <code>.env</code>.</p>
<h3 id="heading-gemini-model-not-found-404">Gemini model not found (404)</h3>
<pre><code class="language-json">{ "error": { "code": 404, "message": "models/text-embedding-004 is not found" } }
</code></pre>
<p>The Google AI model naming has changed. Older tutorials and blog posts reference model names that no longer exist on the v1 endpoint. The correct model for this stack is <code>gemini-embedding-001</code>. That's what this repo uses.</p>
<p>If you want to see every model available to your API key, run:</p>
<pre><code class="language-bash">node list-models.js
</code></pre>
<p>That script fetches the live list directly from the API so you're not guessing.</p>
<h3 id="heading-vector-dimension-mismatch">Vector dimension mismatch</h3>
<pre><code class="language-plaintext">ERROR: expected 768 dimensions, not 3072
</code></pre>
<p>This error appears when your database table was created with a different dimension count than what your embedding model produces. <code>gemini-embedding-001</code> outputs 3,072-dimensional vectors. The <code>documents</code> table in this tutorial uses <code>VECTOR(3072)</code> to match.</p>
<p>If you get this error, it means either an old table exists with the wrong dimension, or you changed embedding models without recreating the table. Drop the data volume and restart:</p>
<pre><code class="language-bash">docker compose down -v
docker compose up -d
</code></pre>
<h3 id="heading-vector-index-dimension-limit">Vector index dimension limit</h3>
<pre><code class="language-plaintext">ERROR: ivfflat index type only supports up to 2000 dimensions
</code></pre>
<p>pgvector's <code>ivfflat</code> and <code>hnsw</code> index types have a maximum dimension of 2000. Since <code>gemini-embedding-001</code> produces 3,072-dimensional vectors, neither index type works.</p>
<p>This tutorial drops the index and lets pgvector do a full scan — fine for development and any reasonably sized corpus. Scaling to thousands of documents in production? Pick a model under 2000 dimensions. OpenAI's <code>text-embedding-3-small</code> outputs 1536 and plays nicely with both index types.</p>
<h3 id="heading-port-3000-is-already-in-use">Port 3000 is already in use</h3>
<pre><code class="language-plaintext">Error: EADDRINUSE: address already in use :::3000
</code></pre>
<p>Some other process got there first. Swap the port number in <code>.env</code>:</p>
<pre><code class="language-plaintext">PORT=3002
</code></pre>
<p>Save it and restart the server.</p>
<h3 id="heading-gemini-generation-returns-quota-exceeded-limit-0">Gemini generation returns quota exceeded (limit: 0)</h3>
<pre><code class="language-json">{ "error": { "status": "RESOURCE_EXHAUSTED", "message": "Quota exceeded for quota metric ... with limit 0" } }
</code></pre>
<p>That <code>limit 0</code> means Google has switched off free generation in your country entirely — not that you've used it up. I hit this myself while testing from Pakistan.</p>
<p>That's exactly why this tutorial uses Groq instead. Make sure <code>GROQ_API_KEY</code> is in your <code>.env</code> and that <code>generateAnswer</code> in <code>src/embeddings.js</code> is pointing at <code>api.groq.com</code>.</p>
<p>To verify both keys work, run:</p>
<pre><code class="language-bash">node test-keys.js
</code></pre>
<p>It tests the Gemini embedding endpoint and the Groq generation endpoint independently and reports whether each succeeds.</p>
<h3 id="heading-nodemon-doesnt-pick-up-changes-to-env">nodemon doesn't pick up changes to <code>.env</code></h3>
<p>nodemon only watches <code>.js</code> files — <code>.env</code> changes don't trigger a restart. Switch to the terminal running the server and type <code>rs</code>, then hit Enter. That forces a restart and picks up whatever you changed.</p>
<h3 id="heading-curl-doesnt-work-in-windows-powershell"><code>curl</code> doesn't work in Windows PowerShell</h3>
<pre><code class="language-plaintext">curl : The term 'curl' is not recognized
</code></pre>
<p>or</p>
<pre><code class="language-plaintext">curl : Cannot bind parameter because parameter 'Method' is specified more than once
</code></pre>
<p>PowerShell has a built-in <code>curl</code> alias that points to <code>Invoke-WebRequest</code> — completely different flags, completely different behaviour. Add <code>.exe</code> and you bypass the alias and hit the real binary.</p>
<p>So instead of <code>curl</code>, type <code>curl.exe</code>:</p>
<pre><code class="language-powershell"># Ingest
curl.exe -X POST http://localhost:3000/ingest -F "file=@your-document.pdf"

# Chat
curl.exe -X POST http://localhost:3000/chat -H "Content-Type: application/json" -d "{\"question\": \"How do I handle async errors?\"}"
</code></pre>
<p>That <code>.exe</code> is the whole fix.</p>
<h3 id="heading-docker-desktop-stopped-running">Docker Desktop stopped running</h3>
<p>Docker Desktop doesn't start automatically after a reboot on most setups. If your Docker commands suddenly fail with connection errors, that's probably why. Open Docker Desktop, wait until it says "Engine running", then try again.</p>
<h2 id="heading-how-to-swap-in-openai">How to Swap in OpenAI</h2>
<p>If you want to use the OpenAI API instead of Gemini, it's three changes.</p>
<p>1. Install the OpenAI SDK:</p>
<pre><code class="language-bash">npm install openai
</code></pre>
<p>2. Replace <code>src/embeddings.js</code> entirely:</p>
<pre><code class="language-javascript">const OpenAI = require('openai');

const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

async function embedText(text) {
  const result = await client.embeddings.create({
    model: 'text-embedding-3-small',
    input: text,
  });
  return result.data[0].embedding;
}

async function generateAnswer(context, question) {
  const result = await client.chat.completions.create({
    model: 'gpt-4o-mini',
    messages: [
      { role: 'system', content: 'Answer only from the context provided. If the context is insufficient, say so.' },
      { role: 'user', content: `Context:\n${context}\n\nQuestion: ${question}` },
    ],
  });
  return result.choices[0].message.content;
}

module.exports = { embedText, generateAnswer };
</code></pre>
<p>3. Update the vector dimension in <code>src/db.js</code>:</p>
<p>Open <code>db.js</code> and swap <code>VECTOR(3072)</code> for <code>VECTOR(1536)</code> — that's the output size of <code>text-embedding-3-small</code>. Then kill the volume so the table gets recreated with the right dimensions:</p>
<pre><code class="language-bash">docker compose down -v
docker compose up -d
</code></pre>
<p>Nothing else needs touching. The ingestion and query logic works the same regardless of which model you plugged in.</p>
<h2 id="heading-what-to-build-next">What to Build Next</h2>
<p>What you've built works. But there are some gaps that come up quickly once you put it in front of real users.</p>
<p>The most noticeable one is <strong>streaming</strong>. Right now <code>/chat</code> holds the connection open until Groq finishes generating the full answer, then returns everything at once. On a short question that's fine. On a longer one, the user stares at nothing for a few seconds and wonders if the request hung.</p>
<p>The Groq API supports streaming — add <code>stream: true</code> to the request body and tokens start coming back as they're generated. Piping those through Express with <code>res.write()</code> is maybe 15 minutes of work and the difference in feel is immediate.</p>
<p><strong>Metadata filtering</strong> is the second thing you'll want. Once you've loaded more than a few documents, queries bleed across everything: ask about the API spec and you'll get chunks from the onboarding guide too.</p>
<p>The fix is a <code>metadata JSONB</code> column where you store the document ID on ingest, then add <code>WHERE metadata-&gt;&gt;'doc_id' = $1</code> to the similarity query. Expose it as an optional body field on <code>/chat</code>: <code>{ "question": "...", "docId": "api-spec-v2" }</code>. Users get scoped results, and you get much cleaner answers.</p>
<p>When your corpus grows into the hundreds of documents, look at <strong>re-ranking</strong>. Vector similarity retrieval is fast but approximate — it finds chunks that are semantically close to the question, not necessarily the ones that most directly answer it.</p>
<p>The pattern is: retrieve the top 20 by cosine distance, then run a cross-encoder over them to re-score by actual relevance, then take the best 5 from that second pass. LangChain.js has a cross-encoder wrapper if you don't want to implement it yourself.</p>
<p>The last thing most people forget until they actually need it is <strong>document management</strong> — the ability to list what's ingested, delete a specific file, and re-ingest an updated version.</p>
<p>A <code>DELETE FROM documents WHERE source = $1</code> handles the delete case. Add a <code>GET /documents</code> endpoint that queries <code>SELECT DISTINCT source FROM documents</code> and you have a complete enough API for real use.</p>
<p>RAG isn't magic. It's a well-scoped retrieval problem combined with a language model that's been told to stay within its lane.</p>
<p>The quality of your answers depends on three things: how cleanly your PDFs parse, how well your chunk size fits the content type, and how clearly your system prompt instructs the model to say "I don't know" rather than guess. Get those right and you've built something genuinely useful: the kind of thing that saves a new engineer's first two weeks.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Browser-Based PDF Image Extractor Using JavaScript ]]>
                </title>
                <description>
                    <![CDATA[ PDF files are widely used for sharing documents because they preserve formatting across different devices. Many PDFs contain valuable images such as logos, product photos, charts, diagrams, illustrati ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-pdf-image-extractor-javascript/</link>
                <guid isPermaLink="false">6a54f26925b48b98bd11b23e</guid>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ pdf ]]>
                    </category>
                
                    <category>
                        <![CDATA[ pdfjs ]]>
                    </category>
                
                    <category>
                        <![CDATA[ webdev ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Bhavin Sheth ]]>
                </dc:creator>
                <pubDate>Mon, 13 Jul 2026 14:12:57 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/e8926aef-8f78-4f09-92f1-7bbb7beb8b68.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>PDF files are widely used for sharing documents because they preserve formatting across different devices. Many PDFs contain valuable images such as logos, product photos, charts, diagrams, illustrations, and marketing graphics.</p>
<p>While these images are easy to view, extracting them individually isn't always simple. Many users rely on screenshots or manual cropping, which can reduce image quality and take unnecessary time.</p>
<p>In this tutorial, you'll build a browser-based <strong>PDF Image Extractor</strong> using JavaScript. The application lets users upload a PDF, preview its pages, extract embedded images, organize them by page, and download individual images or all extracted images at once.</p>
<p>Everything runs directly inside the browser, so uploaded documents never leave the user's device. This makes the tool fast, private, and easy to use without requiring a backend server.</p>
<p>By the end of this tutorial, you'll have a fully functional PDF Image Extractor capable of recovering embedded images while preserving their original quality.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-why-extract-images-from-pdfs">Why Extract Images from PDFs?</a></p>
</li>
<li><p><a href="#heading-how-images-are-stored-inside-pdf-files">How Images Are Stored Inside PDF Files</a></p>
</li>
<li><p><a href="#heading-understanding-embedded-images-vs-rendered-pages">Understanding Embedded Images vs Rendered Pages</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-creating-the-upload-interface">Creating the Upload Interface</a></p>
</li>
<li><p><a href="#heading-previewing-uploaded-pdf-pages">Previewing Uploaded PDF Pages</a></p>
</li>
<li><p><a href="#heading-finding-embedded-images">Finding Embedded Images</a></p>
</li>
<li><p><a href="#heading-extracting-images-from-pdf-pages">Extracting Images from PDF Pages</a></p>
</li>
<li><p><a href="#heading-displaying-extracted-images">Displaying Extracted Images</a></p>
</li>
<li><p><a href="#heading-downloading-individual-images">Downloading Individual Images</a></p>
</li>
<li><p><a href="#heading-downloading-all-images-from-the-pdf">Downloading All Images from the PDF</a></p>
</li>
<li><p><a href="#heading-demo-how-the-pdf-image-extractor-works">Demo: How the PDF Image Extractor Works</a></p>
</li>
<li><p><a href="#heading-performance-optimization-tips">Performance Optimization Tips</a></p>
</li>
<li><p><a href="#heading-important-notes-from-real-world-use">Important Notes from Real-World Use</a></p>
</li>
<li><p><a href="#heading-common-mistakes-to-avoid">Common Mistakes to Avoid</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-why-extract-images-from-pdfs">Why Extract Images from PDFs?</h2>
<p>Although PDF documents are primarily designed for sharing text-based information, they often contain valuable visual assets. Product catalogs include product photographs, annual reports contain charts and graphs, presentations use icons and illustrations, brochures showcase marketing banners, and technical manuals include diagrams and engineering drawings.</p>
<p>Without a dedicated image extraction tool, users often take screenshots or manually crop pages to save these visuals. Unfortunately, screenshots usually reduce image quality, introduce unwanted page elements, and require significant manual effort when working with large documents.</p>
<p>A PDF Image Extractor automates this process by identifying every embedded image inside the document and separating it from the surrounding page content. Instead of copying an entire page, users receive individual image files that can be downloaded and reused immediately.</p>
<p>This capability is extremely useful across many industries.</p>
<p>Graphic designers frequently receive client brochures, advertisements, and presentation files that contain logos, icons, or promotional graphics. Instead of recreating those assets manually, they can extract the original images directly from the PDF and continue working with high-quality source files.</p>
<p>Marketing teams often work with catalogs, product flyers, promotional leaflets, and campaign reports. Extracting product photographs or promotional graphics saves considerable time when creating social media posts, advertisements, landing pages, or newsletters.</p>
<p>Publishers and content creators regularly receive PDF magazines, ebooks, newsletters, and educational material containing illustrations and infographics. Individual images can be extracted and reused without manually cropping every page.</p>
<p>Researchers frequently download scientific papers containing graphs, charts, microscopy images, satellite photographs, and experimental diagrams. Image extraction allows them to save those visuals separately for presentations, publications, or further analysis.</p>
<p>Educational institutions use image extraction when preparing teaching material. Teachers can reuse diagrams, mathematical figures, scientific illustrations, historical maps, or educational graphics from reference documents without recreating them from scratch.</p>
<p>Government departments often maintain scanned archives containing seals, stamps, signatures, photographs, maps, engineering plans, and official diagrams. Extracting these images individually simplifies document digitization and archival workflows.</p>
<p>E-commerce businesses can also benefit significantly from image extraction. For example, a seller may receive a supplier catalog in PDF format containing hundreds of product photographs. Instead of requesting every image separately, the seller can extract all embedded product images from the catalog within minutes and reuse them while preparing listings for platforms such as Amazon, Flipkart, Meesho, Shopify, or WooCommerce.</p>
<p>Businesses working with invoices and purchase documents can also recover company logos, QR codes, signatures, and barcode images for document verification or automation systems.</p>
<p>Because this application performs every operation locally inside the browser, sensitive business documents remain private while users quickly recover all embedded images from their PDFs.</p>
<h2 id="heading-how-images-are-stored-inside-pdf-files">How Images Are Stored Inside PDF Files</h2>
<p>Many people assume that a PDF page is simply a picture of the document. In reality, PDF files are much more sophisticated.</p>
<p>Each page inside a PDF is built from multiple independent objects. Text is stored separately using fonts and character information. Lines and shapes are represented as vector drawing instructions. Images are embedded as independent image objects that are placed at specific positions on the page.</p>
<p>This separation is one of the reasons PDFs remain flexible and efficient. A document may contain dozens of pages while reusing the same company logo or icon multiple times without storing duplicate copies.</p>
<p>When an image is embedded inside a PDF, the file usually preserves information such as the image dimensions, color space, compression method, and image format. Depending on how the document was created, embedded images may use formats such as JPEG, PNG, JPEG2000, CCITT, or other PDF-supported image encodings.</p>
<p>A PDF Image Extractor scans the internal structure of the document to locate these embedded image objects. Instead of capturing the entire page as a screenshot, it retrieves each image individually whenever possible.</p>
<p>This approach preserves much higher quality because the original embedded image is recovered rather than recreating it from the rendered page.</p>
<p>Understanding how PDF files store images also explains why some documents contain dozens of extractable images while others contain none at all. If a PDF consists entirely of vector graphics or text, there may be no embedded raster images available for extraction.</p>
<p>Knowing the difference between these document structures helps developers build more accurate PDF processing tools while helping users understand the capabilities and limitations of image extraction.</p>
<h2 id="heading-understanding-embedded-images-vs-rendered-pages">Understanding Embedded Images vs Rendered Pages</h2>
<p>One of the most common misconceptions about PDF image extraction is that every visible picture on a page can always be extracted as a separate image.</p>
<p>In reality, there is an important difference between <strong>embedded images</strong> and <strong>rendered page images</strong>.</p>
<p>An embedded image is an independent object stored inside the PDF document. These images usually retain their original quality and can often be extracted without any loss of resolution.</p>
<p>A rendered page, on the other hand, is simply a visual representation of everything that appears on the page. When PDF.js displays a page inside the browser, it combines text, vector graphics, images, backgrounds, and shapes into a single canvas. Although this rendered page looks identical to the original document, it's no longer separated into individual components.</p>
<p>For example, imagine a product catalog containing a company logo, five product photographs, several icons, and descriptive text.</p>
<p>The PDF page preview displays everything together as one complete page. However, the PDF Image Extractor scans the document internally and identifies the individual logo, each product photograph, and every embedded icon separately. This allows users to download each image individually instead of cropping screenshots from the page preview.</p>
<p>Another important point is that not every visible graphic is actually an image.</p>
<p>Some company logos are created entirely using vector drawing commands. Charts may also be generated using vector graphics rather than bitmap images. Since these objects are not stored as raster images, they can't always be extracted using an image extraction tool.</p>
<p>Understanding this distinction helps explain why image extraction results may differ between documents even when they appear visually similar.</p>
<p>For developers, learning how embedded resources differ from rendered pages provides a much deeper understanding of PDF internals and browser-based document processing.</p>
<h2 id="heading-project-setup">Project Setup</h2>
<p>We'll build the PDF Image Extractor using standard web technologies so that the entire application runs directly inside the browser without requiring a backend server.</p>
<p>The project consists of a simple HTML file for the interface, a CSS file for styling, and a JavaScript file that handles PDF loading, page rendering, image extraction, and downloading.</p>
<p>Create the following project structure:</p>
<pre><code class="language-text">pdf-image-extractor/

│── index.html

│── style.css

│── script.js

│── assets/
</code></pre>
<p>After creating the project, include the required JavaScript libraries inside your <strong>index.html</strong> file:</p>
<pre><code class="language-html">&lt;script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.4.168/pdf.min.js"&gt;&lt;/script&gt;

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

&lt;script src="script.js"&gt;&lt;/script&gt;
</code></pre>
<p>Once these files are ready, the browser will have everything required to read PDF files, render document pages, inspect PDF objects, locate embedded images, and generate downloadable image files.</p>
<p>Keeping the project lightweight also makes it easier to understand each stage of the image extraction workflow.</p>
<h2 id="heading-what-libraries-are-we-using">What Libraries Are We Using?</h2>
<p>Extracting images from PDF documents requires more than simply displaying PDF pages inside the browser. The application needs to load the document, inspect its internal structure, render preview pages, and recover embedded image objects.</p>
<p>To accomplish this, we'll use two JavaScript libraries.</p>
<p>The first library is <strong>PDF.js</strong>.</p>
<p>PDF.js is Mozilla's open-source PDF rendering engine. It allows browsers to load PDF documents without additional plugins and provides APIs for reading document pages, rendering previews, accessing page objects, and inspecting document resources.</p>
<p>In this project, PDF.js is responsible for loading the uploaded PDF and generating the page preview shown to the user before image extraction begins.</p>
<p>The second library is <strong>PDF-lib</strong>.</p>
<p>PDF-lib provides low-level access to PDF objects and document resources. Although it's widely used for editing PDF files, it's also useful when working with embedded objects and document manipulation. It complements PDF.js by giving developers additional flexibility when extending the application with future PDF editing features.</p>
<p>Together, these libraries allow us to create a browser-based image extraction workflow that is fast, secure, and completely client-side.</p>
<p>The following code initializes PDF.js:</p>
<pre><code class="language-javascript">pdfjsLib.GlobalWorkerOptions.workerSrc =

"https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.4.168/pdf.worker.min.js";
</code></pre>
<p>Loading the worker separately keeps PDF rendering responsive while large documents are processed.</p>
<h2 id="heading-creating-the-upload-interface">Creating the Upload Interface</h2>
<p>Every document processing application begins with uploading a file.</p>
<p>The upload interface is responsible for accepting PDF documents, validating the selected file, and preparing it for processing. A clean upload experience is important because it becomes the entry point for the entire application.</p>
<p>In this project, users can either drag a PDF onto the upload area or browse for a document using the standard file picker.</p>
<p>Once the file has been selected, the browser immediately verifies that it's a valid PDF before attempting to load it.</p>
<p>Supporting both drag-and-drop uploads and manual file selection provides a familiar experience across desktop and mobile devices.</p>
<p>The upload section also displays clear instructions so first-time users understand exactly how to begin extracting images.</p>
<p>Create the upload area using the following HTML:</p>
<pre><code class="language-html">&lt;div id="dropZone" class="drop-zone"&gt;

    &lt;div class="upload-icon"&gt;

        ☁

    &lt;/div&gt;

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

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

    &lt;button id="selectBtn"&gt;

        Select PDF

    &lt;/button&gt;

    &lt;input

        type="file"

        id="pdfFile"

        accept="application/pdf"

        hidden&gt;

&lt;/div&gt;
</code></pre>
<p>Next, validate the uploaded document:</p>
<pre><code class="language-javascript">const file = pdfFile.files[0];

if(!file){

    return;

}

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

    alert("Please upload a valid PDF.");

    return;

}
</code></pre>
<p>After validation succeeds, the browser reads the uploaded file:</p>
<pre><code class="language-javascript">const buffer =

await file.arrayBuffer();

loadPDF(buffer);
</code></pre>
<p>At this point, the PDF has been loaded into memory and is ready for preview generation.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/fbd94f23-f27b-47dc-ad2b-bb98db7bd219.png" alt="PDF upload interface allowing users to drag and drop or browse for a PDF before extracting embedded images." style="display:block;margin:0 auto" width="538" height="592" loading="lazy">

<h2 id="heading-previewing-uploaded-pdf-pages">Previewing Uploaded PDF Pages</h2>
<p>Before extracting images, users should first verify that they uploaded the correct document.</p>
<p>Instead of immediately scanning the PDF, the application generates thumbnail previews for every page. These previews help users confirm page order, inspect the document contents, and estimate where embedded images are located.</p>
<p>Preview generation is particularly useful for large reports, product catalogs, presentations, magazines, brochures, ebooks, and technical documentation containing dozens of pages.</p>
<p>Each preview is rendered using PDF.js and displayed inside a responsive grid layout.</p>
<p>First, load the uploaded document:</p>
<pre><code class="language-javascript">const pdf = await pdfjsLib

.getDocument({

    data:buffer

})

.promise;
</code></pre>
<p>Next, loop through every page:</p>
<pre><code class="language-javascript">for(

let pageNumber = 1;

pageNumber &lt;= pdf.numPages;

pageNumber++

){

    renderPage(pageNumber);

}
</code></pre>
<p>Render the page as a canvas:</p>
<pre><code class="language-javascript">const page =

await pdf.getPage(pageNumber);

const viewport =

page.getViewport({

    scale:0.35

});

const canvas =

document.createElement("canvas");

canvas.width = viewport.width;

canvas.height = viewport.height;

await page.render({

    canvasContext:

    canvas.getContext("2d"),

    viewport

}).promise;
</code></pre>
<p>Finally, add the preview to the page:</p>
<pre><code class="language-javascript">previewContainer

.appendChild(canvas);
</code></pre>
<p>Once rendering is complete, every page becomes visible inside the preview area.</p>
<p>Users can scroll through the thumbnails and verify that the correct document has been uploaded before starting the extraction process.</p>
<p>Although the preview displays complete page images, no extraction has occurred yet. The next stage scans the internal PDF structure to locate every embedded image separately.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/ca8a11f0-f242-45f8-bfc8-83b18f97b50e.png" alt="PDF page preview displaying uploaded document pages before image extraction begins." style="display:block;margin:0 auto" width="533" height="397" loading="lazy">

<h3 id="heading-why-previewing-the-pdf-matters">Why Previewing the PDF Matters</h3>
<p>Previewing the uploaded document may seem like a small feature, but it greatly improves the overall user experience.</p>
<p>Without a preview, users have no way to verify that they selected the correct file before extraction begins. This becomes especially important when working with multiple PDFs that have similar filenames.</p>
<p>Page previews also help users estimate where images appear throughout the document. For example, a product catalog may contain photographs only on certain pages, while a technical report might include diagrams only within specific chapters.</p>
<p>By reviewing the page thumbnails first, users gain confidence that the document is correct before the application begins scanning for embedded images.</p>
<p>This simple verification step reduces mistakes, avoids unnecessary processing, and makes the overall workflow feel much more intuitive.</p>
<h2 id="heading-finding-embedded-images">Finding Embedded Images</h2>
<p>Once the PDF pages have been rendered and displayed in the preview section, the application is ready to search for embedded images.</p>
<p>This stage is different from generating page previews. The preview simply renders each page as a complete visual image, while the extraction process examines the internal structure of the PDF to locate every embedded image object stored inside the document.</p>
<p>Each page is scanned individually. If embedded images are found, the application records their location, dimensions, image format, and page number before preparing them for extraction.</p>
<p>This page-by-page approach makes it easier to organize the extracted images later and allows users to understand exactly where each image originated within the document.</p>
<p>Scanning only begins after users click the <strong>Extract Images</strong> button, ensuring that unnecessary processing is avoided if they simply want to preview the document.</p>
<p>First, create the click event for the extraction button:</p>
<pre><code class="language-javascript">document

.getElementById(

"extractBtn"

)

.addEventListener(

"click",

extractImages

);
</code></pre>
<p>Next, loop through every page inside the uploaded PDF:</p>
<pre><code class="language-javascript">for(

let pageNumber = 1;

pageNumber &lt;= pdf.numPages;

pageNumber++

){

    const page =

    await pdf.getPage(

    pageNumber

    );

}
</code></pre>
<p>Read the page operator list:</p>
<pre><code class="language-javascript">const operatorList =

await page.getOperatorList();
</code></pre>
<p>Now inspect every drawing operation to determine whether it contains an embedded image:</p>
<pre><code class="language-javascript">operatorList.fnArray

.forEach(operation=&gt;{

    if(

    operation ===

    pdfjsLib.OPS.paintImageXObject

    ){

        console.log(

        "Image Found"

        );

    }

});
</code></pre>
<p>After every page has been scanned, the application creates a collection containing all discovered images grouped by page.</p>
<p>This collection becomes the foundation for the extraction process that follows.</p>
<h2 id="heading-extracting-images-from-pdf-pages">Extracting Images from PDF Pages</h2>
<p>Once embedded image objects have been identified, the application begins extracting them from the PDF.</p>
<p>Unlike taking screenshots of an entire page, this method retrieves each embedded image individually whenever possible. As a result, the extracted images preserve their original quality, dimensions, and compression rather than inheriting the resolution of the page preview.</p>
<p>Each image is assigned to the page where it was found. Organizing images this way makes it much easier for users to locate graphics inside large reports, magazines, brochures, presentations, catalogs, technical manuals, and research papers.</p>
<p>As each page finishes processing, its extracted images are stored inside a JavaScript array before being displayed inside the browser.</p>
<p>Create an array for storing extracted images:</p>
<pre><code class="language-javascript">const extractedImages = [];
</code></pre>
<p>Save every discovered image:</p>
<pre><code class="language-javascript">extractedImages.push({

    page:

    pageNumber,

    image:

    imageData,

    type:

    imageType

});
</code></pre>
<p>Each stored object contains useful information that will later be displayed to the user.</p>
<p>The extraction routine continues until every page inside the uploaded PDF has been inspected.</p>
<p>Once complete, the browser immediately displays the extracted images without requiring another processing step.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/4f9129c9-afcf-42a7-b804-fdc36c23861c.png" alt="Extract Images button used to begin scanning the uploaded PDF for embedded images." style="display:block;margin:0 auto" width="539" height="69" loading="lazy">

<h2 id="heading-displaying-extracted-images">Displaying Extracted Images</h2>
<p>After extraction finishes, the application presents every recovered image inside an organized gallery.</p>
<p>Instead of displaying one long collection of images, the results are grouped according to the page from which they were extracted. This organization makes it much easier to understand the original document structure.</p>
<p>For every extracted image, the application displays a preview together with useful technical information.</p>
<p>Users can immediately see the image dimensions, image format, and the page where the image originated. This additional information helps determine whether an image is suitable for reuse before downloading it.</p>
<p>For example, a high-resolution product photograph may be useful for marketing material, while a small company logo may only be appropriate for branding purposes.</p>
<p>Loop through every extracted image:</p>
<pre><code class="language-javascript">extractedImages.forEach(image=&gt;{

    renderImageCard(

    image

    );

});
</code></pre>
<p>Create the preview card:</p>
<pre><code class="language-javascript">const card =

document.createElement(

"div"

);

card.className =

"image-card";
</code></pre>
<p>Insert the preview:</p>
<pre><code class="language-javascript">const img =

document.createElement(

"img"

);

img.src =

image.image;
</code></pre>
<p>Display the image information:</p>
<pre><code class="language-javascript">details.innerHTML =

`

Dims:

${image.width} × ${image.height}

&lt;br&gt;

Type:

${image.type}

`;
</code></pre>
<p>Once every card has been created, the gallery displays all extracted images grouped beneath their respective pages.</p>
<p>This layout provides a clean overview of every image contained inside the uploaded PDF while making individual downloads straightforward.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/2702a964-fb6d-4b4c-8e4d-d74795650926.png" alt="Extracted images displayed page by page with preview, dimensions, image format, and download buttons." style="display:block;margin:0 auto" width="634" height="687" loading="lazy">

<h2 id="heading-downloading-individual-images">Downloading Individual Images</h2>
<p>Many users don't need every image contained inside a PDF.</p>
<p>For example, a designer may only want the company logo from a brochure, while a researcher may only need one graph from a scientific paper.</p>
<p>To support these workflows, every extracted image includes its own download button.</p>
<p>When clicked, the browser downloads only the selected image without affecting any of the remaining extracted images.</p>
<p>This allows users to quickly save exactly the graphics they need.</p>
<p>Create a download button:</p>
<pre><code class="language-javascript">const button =

document.createElement(

"button"

);

button.innerText =

"Download";
</code></pre>
<p>Attach the download event:</p>
<pre><code class="language-javascript">button.onclick = ()=&gt;{

    downloadImage(

    image

    );

};
</code></pre>
<p>Generate the download:</p>
<pre><code class="language-javascript">const link =

document.createElement(

"a"

);

link.href =

image.image;

link.download =

image.fileName;

link.click();
</code></pre>
<p>Providing separate download buttons makes the application much more flexible because users can save only the images they actually need instead of downloading every extracted asset.</p>
<h2 id="heading-downloading-all-images-from-the-pdf">Downloading All Images from the PDF</h2>
<p>Large PDF documents often contain dozens or even hundreds of embedded images.</p>
<p>Downloading every image individually would be both slow and repetitive.</p>
<p>To simplify this workflow, the application also includes a <strong>Download All Images from PDF</strong> button.</p>
<p>After extraction has completed, clicking this button automatically downloads every extracted image from every page.</p>
<p>This feature is particularly useful when working with supplier catalogs, product brochures, magazines, annual reports, technical documentation, ebooks, educational material, and presentation files containing many graphics.</p>
<p>Loop through every extracted image:</p>
<pre><code class="language-javascript">extractedImages.forEach(image=&gt;{

    downloadImage(

    image

    );

});
</code></pre>
<p>Attach the click event:</p>
<pre><code class="language-javascript">downloadAllButton

.addEventListener(

"click",

downloadAllImages

);
</code></pre>
<p>Finally, allow users to begin another extraction:</p>
<pre><code class="language-javascript">startOverButton

.addEventListener(

"click",

resetApplication

);
</code></pre>
<p>After the downloads have completed, users can click <strong>Start Over</strong> to upload another PDF and repeat the extraction process without refreshing the browser.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/09c1107c-2f44-429e-bbdf-86ea9b1731b7.png" alt="Download All Images from PDF button with Start Over option displayed after image extraction completes." style="display:block;margin:0 auto" width="558" height="90" loading="lazy">

<h2 id="heading-demo-how-the-pdf-image-extractor-works">Demo: How the PDF Image Extractor Works</h2>
<h3 id="heading-step-1-upload-your-pdf-document">Step 1: Upload Your PDF Document</h3>
<p>The image extraction workflow begins by uploading a PDF document using either the drag-and-drop area or the file picker.</p>
<p>Once a document has been selected, the browser validates that the uploaded file is a PDF before reading it into memory. Since the application performs all processing locally, the uploaded document never leaves the user's computer, making the tool suitable for confidential business reports, catalogs, presentations, contracts, technical manuals, research papers, and other sensitive documents.</p>
<p>After the PDF has been loaded successfully, the application prepares every page for preview generation before image extraction begins.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/e6795311-e452-47d1-b273-b7055a4a1cc4.png" alt="PDF upload interface allowing users to drag and drop or browse for a PDF document before extracting embedded images." style="display:block;margin:0 auto" width="538" height="592" loading="lazy">

<h3 id="heading-step-2-preview-uploaded-pdf-pages">Step 2: Preview Uploaded PDF Pages</h3>
<p>After the upload is complete, the application renders thumbnail previews for every page inside the document.</p>
<p>Instead of immediately scanning the PDF for images, users first receive a visual overview of the entire document. This allows them to verify that they selected the correct PDF and quickly identify which pages contain photographs, diagrams, illustrations, charts, or other graphics.</p>
<p>Page previews are especially useful when working with large product catalogs, magazines, annual reports, brochures, technical documentation, educational books, and research papers containing dozens or even hundreds of pages.</p>
<p>This verification step helps prevent unnecessary processing and improves the overall user experience.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/c1a4d26e-0526-495b-aa9a-69b722469c83.png" alt=" Uploaded PDF page preview displaying page thumbnails before image extraction begins." style="display:block;margin:0 auto" width="533" height="397" loading="lazy">

<h3 id="heading-step-3-extract-embedded-images">Step 3: Extract Embedded Images</h3>
<p>Once the document has been verified, users click the <strong>Extract Images</strong> button to begin scanning the PDF.</p>
<p>The application examines every page individually, searching for embedded image objects stored inside the document. Unlike screenshots or page rendering, the extractor retrieves the original image resources whenever possible, preserving their quality and dimensions.</p>
<p>As each page is processed, every discovered image is grouped according to the page where it originally appeared. This organization makes it much easier to browse the extracted results later.</p>
<p>Depending on the size of the PDF and the number of embedded images, extraction may take a few seconds for larger documents.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/b846ceef-5020-42d6-875c-82e1df4b1d48.png" alt="Extract Images button used to begin scanning the uploaded PDF for embedded images." style="display:block;margin:0 auto" width="539" height="69" loading="lazy">

<h3 id="heading-step-4-review-the-extracted-images">Step 4: Review the Extracted Images</h3>
<p>After extraction is complete, the browser displays every recovered image inside an organized gallery.</p>
<p>Instead of showing one large collection of images, the application groups the results page by page. This allows users to understand exactly where each image came from within the original document.</p>
<p>Every image card displays a preview together with useful information such as the page number, image dimensions, and image format. This helps users quickly identify the graphics they need before downloading anything.</p>
<p>For example, a brochure may contain company logos, banners, icons, and product photographs spread across several pages. Grouping images by page makes navigating these documents much easier.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/7fedacb3-c9b3-46be-a3f0-61ffc3b95ee4.png" alt="Extracted PDF images organized page by page with previews, dimensions, image type, and download buttons." style="display:block;margin:0 auto" width="634" height="687" loading="lazy">

<h3 id="heading-step-5-download-individual-images-or-pagewise">Step 5: Download Individual Images or Pagewise</h3>
<p>Each extracted image includes its own <strong>Download Image</strong> button.</p>
<p>This feature is useful when users only need one or two graphics from a large document. Instead of downloading every extracted image, they can save only the specific illustrations, charts, product photographs, or logos that are relevant to their work.</p>
<p>For example, a designer may only need a company logo, while a marketing team may only want product images from a supplier catalog. Individual downloads eliminate unnecessary files and simplify the workflow.</p>
<p>After clicking the download button, the browser immediately saves the selected image without requiring any additional processing.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/b56e0d09-cad8-4245-8482-d811ce33b30a.png" alt="Individual download button displayed beneath each extracted image." style="display:block;margin:0 auto" width="233" height="100" loading="lazy">

<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/d3b93e5d-2e95-4635-8c17-f5070207d453.png" alt="Individual download button displayed beneath each extracted image." style="display:block;margin:0 auto" width="533" height="65" loading="lazy">

<h3 id="heading-step-6-download-every-image">Step 6: Download Every Image</h3>
<p>For users who need all graphics contained inside the document, the application also provides a <strong>Download All Images from PDF</strong> button.</p>
<p>After extraction has finished, clicking this button automatically downloads every recovered image from every page. This saves considerable time compared to downloading each image individually.</p>
<p>This feature is particularly useful when processing large product catalogs, marketing brochures, presentation decks, educational books, technical manuals, magazines, supplier catalogs, or company reports containing dozens of embedded images.</p>
<p>Once the downloads are complete, users can click <strong>Start Over</strong> to clear the current session and upload another PDF without refreshing the page.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/f58bbf48-fa0c-4ae3-b095-4efb4c9b36da.png" alt="Download All Images from PDF button with Start Over option after image extraction completes." style="display:block;margin:0 auto" width="379" height="90" loading="lazy">

<h3 id="heading-step-7-start-a-new-extraction">Step 7: Start a New Extraction</h3>
<p>After downloading the required images, users can begin working with another PDF document.</p>
<p>Clicking <strong>Start Over</strong> clears the uploaded document, removes every generated preview, resets the extracted image gallery, and restores the application to its initial state.</p>
<p>This allows users to process multiple PDF files during the same session without reloading the browser or reopening the application.</p>
<p>The reset process is completed instantly, making the workflow smooth and efficient when working with many PDF documents throughout the day.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/76fff769-af0a-4880-ab66-9b2296654f62.png" alt="Start Over button used to reset the application and begin extracting images from another PDF." style="display:block;margin:0 auto" width="175" height="62" loading="lazy">

<h2 id="heading-performance-optimization-tips">Performance Optimization Tips</h2>
<p>Image extraction is generally <a href="https://www.freecodecamp.org/news/build-pdf-ocr-to-text-converter-javascript/">faster than OCR</a> because the application recovers existing image objects instead of recognizing characters. But large PDF documents containing hundreds of pages or high-resolution graphics can still require significant processing time.</p>
<p>One simple optimization is to process pages sequentially instead of attempting to analyze every page simultaneously.</p>
<pre><code class="language-javascript">for(

let page = 1;

page &lt;= pdf.numPages;

page++

){

    await extractPageImages(page);

}
</code></pre>
<p>Loading only the required page into memory reduces browser memory usage and improves stability when processing large documents.</p>
<p>If the application supports page selection, allowing users to extract images from only specific pages can greatly reduce processing time for large catalogs or reports.</p>
<pre><code class="language-javascript">const startPage = 10;

const endPage = 25;
</code></pre>
<p>After images have been downloaded, release any temporary browser resources:</p>
<pre><code class="language-javascript">URL.revokeObjectURL(

imageURL

);
</code></pre>
<p>Finally, clear the extracted image collection before processing another PDF:</p>
<pre><code class="language-javascript">extractedImages.length = 0;
</code></pre>
<p>These small optimizations help the application remain responsive even when working with documents containing hundreds of embedded graphics.</p>
<h2 id="heading-important-notes-from-real-world-use">Important Notes from Real-World Use</h2>
<p>Not every PDF contains embedded images.</p>
<p>Some documents consist entirely of text and vector graphics, while others may contain scanned pages that appear as a single full-page image. Understanding how the original PDF was created helps set realistic expectations before extraction begins.</p>
<p>Always validate uploaded files before processing.</p>
<pre><code class="language-javascript">if(

file.type !== "application/pdf"

){

    alert(

    "Please upload a valid PDF."

    );

}
</code></pre>
<p>Some PDF creators compress embedded images heavily to reduce file size. In those situations, the extracted images will match the quality stored inside the PDF, but they can't be improved beyond the original resolution.</p>
<p>Users should also verify extraction results before downloading every image, particularly when processing large reports or catalogs containing hundreds of graphics.</p>
<p>Because the application performs all operations locally inside the browser, confidential documents remain private throughout the extraction process. This makes browser-based image extraction suitable for business reports, engineering drawings, financial documents, legal records, educational resources, and other sensitive PDFs.</p>
<h2 id="heading-common-mistakes-to-avoid">Common Mistakes to Avoid</h2>
<p>One common mistake is assuming every visible object inside a PDF is an extractable image.</p>
<p>Many diagrams, logos, and charts are actually vector graphics rather than raster images. These elements are rendered by drawing commands and can't always be extracted as standalone image files.</p>
<p>Another mistake is relying on screenshots instead of extracting embedded images.</p>
<p>Screenshots capture only the rendered page displayed on the screen, which often reduces image quality and includes unnecessary page elements.</p>
<p>Always verify that embedded images have been detected before displaying the results.</p>
<pre><code class="language-javascript">if(

extractedImages.length === 0

){

    alert(

    "No embedded images found."

    );

}
</code></pre>
<p>Some users also forget to organize extracted images by page.</p>
<p>Grouping images according to their original page location makes it much easier to navigate large documents containing dozens or hundreds of graphics.</p>
<p>Finally, always review the extracted images before downloading them.</p>
<p>Checking the preview allows users to confirm image quality, dimensions, and page location before saving the files.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this tutorial, you built a browser-based <strong>PDF Image Extractor</strong> using JavaScript.</p>
<p>You learned how to upload PDF files, preview document pages, locate embedded image objects, extract images while preserving their original quality, organize results page by page, download individual images, and download every extracted image directly from the browser.</p>
<p>More importantly, you learned the difference between embedded images and rendered page previews, giving you a better understanding of how PDF documents are structured internally.</p>
<p>Because the entire workflow runs locally inside the browser, users can safely recover graphics from confidential PDF documents without uploading them to external servers.</p>
<p>You can try the complete implementation here:</p>
<p><strong>PDF Image Extractor:</strong> <a href="https://allinonetools.net/extract-images-from-pdf/">https://allinonetools.net/extract-images-from-pdf/</a></p>
<p>Once you understand this workflow, you can extend the project further by adding duplicate image detection, automatic image compression, AI-powered image tagging, background removal, image format conversion, OCR on extracted images, watermark detection, or bulk asset management features.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Implement Role-Based Access Control in a Node.js REST API with JWT ]]>
                </title>
                <description>
                    <![CDATA[ The first time I built an API without thinking about roles, I gave every logged-in user the same access. It worked fine until a regular user accidentally hit a delete endpoint and wiped test data. Tha ]]>
                </description>
                <link>https://www.freecodecamp.org/news/role-based-access-control-nodejs-rest-api-jwt/</link>
                <guid isPermaLink="false">6a4fb4570140649a4367b476</guid>
                
                    <category>
                        <![CDATA[ Node.js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Security ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ api ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Zia Ullah ]]>
                </dc:creator>
                <pubDate>Thu, 09 Jul 2026 14:46:47 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/d742efbd-8170-4fb6-8851-1f7c6ef9125e.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>The first time I built an API without thinking about roles, I gave every logged-in user the same access. It worked fine until a regular user accidentally hit a delete endpoint and wiped test data. That was the day I actually sat down and learned RBAC properly.</p>
<p>Role-Based Access Control sounds fancy, but the idea is simple: what you can do depends on <em>who you are</em>, not just <em>that you're logged in</em>. An admin deletes users. An editor creates posts. A regular user just reads. Same app, completely different experience depending on who's asking.</p>
<p>That's what we're building here. A REST API with three roles: JWT to carry those roles on every request, and a pair of middleware functions that check permissions before your route handlers even run. There's no database hit per request, and no if/else soup in your business logic.</p>
<p>By the end, you'll have three working roles (<code>admin</code>, <code>editor</code>, <code>user</code>) each locked to their own endpoints. More importantly, the pattern is transferable: once it clicks, you'll wire it into your next project without needing a tutorial.</p>
<p><strong>Full source code on GitHub:</strong> <a href="https://github.com/ziaongit/nodejs-rbac-jwt-api">github.com/ziaongit/nodejs-rbac-jwt-api</a></p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-what-youll-learn">What You'll Learn</a></p>
</li>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-what-well-build">What We'll Build</a></p>
</li>
<li><p><a href="#heading-project-setup">Project Setup</a></p>
</li>
<li><p><a href="#heading-setting-up-the-in-memory-data-store">Setting Up the In-Memory Data Store</a></p>
</li>
<li><p><a href="#heading-building-the-auth-routes">Building the Auth Routes</a></p>
</li>
<li><p><a href="#heading-building-the-rbac-middleware">Building the RBAC Middleware</a></p>
</li>
<li><p><a href="#heading-building-the-protected-routes">Building the Protected Routes</a></p>
</li>
<li><p><a href="#heading-putting-it-all-together">Putting It All Together</a></p>
</li>
<li><p><a href="#heading-testing-the-api">Testing the API</a></p>
</li>
<li><p><a href="#heading-key-takeaways">Key Takeaways</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-what-youll-learn">What You'll Learn</h2>
<ul>
<li><p>What RBAC is and how it differs from basic authentication</p>
</li>
<li><p>How to embed roles in JWT payloads</p>
</li>
<li><p>How to write reusable Express middleware for token verification and role checking</p>
</li>
<li><p>How to protect API routes based on user roles</p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<ul>
<li><p>Node.js (v18+) installed</p>
</li>
<li><p>Basic knowledge of Express.js</p>
</li>
<li><p>Familiarity with how JWTs work (we'll cover the relevant parts)</p>
</li>
<li><p>npm installed</p>
</li>
</ul>
<h2 id="heading-what-well-build">What We'll Build</h2>
<p>We'll build a REST API for a simple content management system with three user roles:</p>
<table>
<thead>
<tr>
<th>Role</th>
<th>Permissions</th>
</tr>
</thead>
<tbody><tr>
<td><code>user</code></td>
<td>Read content</td>
</tr>
<tr>
<td><code>editor</code></td>
<td>Read + create content</td>
</tr>
<tr>
<td><code>admin</code></td>
<td>Full access — read, create, delete content, manage users</td>
</tr>
</tbody></table>
<p>The API will expose these endpoints:</p>
<table>
<thead>
<tr>
<th>Method</th>
<th>Endpoint</th>
<th>Access</th>
</tr>
</thead>
<tbody><tr>
<td>POST</td>
<td>/api/auth/register</td>
<td>Public</td>
</tr>
<tr>
<td>POST</td>
<td>/api/auth/login</td>
<td>Public</td>
</tr>
<tr>
<td>GET</td>
<td>/api/content</td>
<td>user, editor, admin</td>
</tr>
<tr>
<td>POST</td>
<td>/api/content</td>
<td>editor, admin</td>
</tr>
<tr>
<td>DELETE</td>
<td>/api/content/:id</td>
<td>admin only</td>
</tr>
<tr>
<td>GET</td>
<td>/api/admin/users</td>
<td>admin only</td>
</tr>
</tbody></table>
<h2 id="heading-project-setup">Project Setup</h2>
<p>Create a new folder and initialize the project:</p>
<pre><code class="language-bash">mkdir nodejs-rbac-jwt-api
cd nodejs-rbac-jwt-api
npm init -y
</code></pre>
<p>Install the dependencies:</p>
<pre><code class="language-bash">npm install express jsonwebtoken bcryptjs dotenv
npm install --save-dev nodemon
</code></pre>
<p>Here's what each package does:</p>
<ul>
<li><p><strong>express</strong>: web framework for building the API</p>
</li>
<li><p><strong>jsonwebtoken</strong>: creates and verifies JWTs</p>
</li>
<li><p><strong>bcryptjs</strong>: securely hashes passwords</p>
</li>
<li><p><strong>dotenv</strong>: reads your <code>.env</code> file so you're not hardcoding secrets in your source code</p>
</li>
</ul>
<p>Update <code>package.json</code> to add start scripts:</p>
<pre><code class="language-json">"scripts": {
  "start": "node src/app.js",
  "dev": "nodemon src/app.js"
}
</code></pre>
<p>Create the project structure:</p>
<pre><code class="language-plaintext">nodejs-rbac-jwt-api/
├── src/
│   ├── middleware/
│   │   └── auth.js
│   ├── routes/
│   │   ├── auth.js
│   │   ├── content.js
│   │   └── admin.js
│   ├── data/
│   │   └── users.js
│   └── app.js
├── .env
├── .env.example
└── package.json
</code></pre>
<p>Create your <code>.env</code> file:</p>
<pre><code class="language-plaintext">JWT_SECRET=your_super_secret_key_change_this_in_production
PORT=3000
</code></pre>
<p><strong>Important:</strong> Never commit your <code>.env</code> file to version control. Add it to <code>.gitignore</code>.</p>
<h2 id="heading-setting-up-the-in-memory-data-store">Setting Up the In-Memory Data Store</h2>
<p>We don't have a database here, just an array in memory. The point was to keep the focus on RBAC, not spend half the tutorial on database config. In a real project, swap the array for whatever database you're already using.</p>
<p>Create <code>src/data/users.js</code>:</p>
<pre><code class="language-javascript">// In-memory users store
// In production, replace this with a real database (MongoDB, PostgreSQL, etc.)
const users = [];

const findUserByEmail = (email) =&gt; users.find((u) =&gt; u.email === email);
const findUserById = (id) =&gt; users.find((u) =&gt; u.id === id);
const createUser = (user) =&gt; {
  users.push(user);
  return user;
};
const getAllUsers = () =&gt; users.map(({ password, ...user }) =&gt; user);

module.exports = { findUserByEmail, findUserById, createUser, getAllUsers };
</code></pre>
<p>One thing worth noting: <code>getAllUsers</code> uses destructuring to drop the password before returning anything. Never send password fields in API responses, even hashed ones.</p>
<h2 id="heading-building-the-auth-routes">Building the Auth Routes</h2>
<p>The auth routes handle registration and login. Login is where roles first enter the picture — we embed the user's role directly into the JWT payload.</p>
<p>Create <code>src/routes/auth.js</code>:</p>
<pre><code class="language-javascript">const express = require('express');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const { findUserByEmail, createUser } = require('../data/users');

const router = express.Router();

// POST /api/auth/register
router.post('/register', async (req, res) =&gt; {
  const { name, email, password, role } = req.body;

  if (!name || !email || !password) {
    return res.status(400).json({ message: 'Name, email, and password are required' });
  }

  if (findUserByEmail(email)) {
    return res.status(409).json({ message: 'Email already registered' });
  }

  // Only allow valid roles — default to 'user' if none provided
  const validRoles = ['user', 'editor', 'admin'];
  const assignedRole = validRoles.includes(role) ? role : 'user';

  const hashedPassword = await bcrypt.hash(password, 10);

  const newUser = {
    id: Date.now().toString(),
    name,
    email,
    password: hashedPassword,
    role: assignedRole,
  };

  createUser(newUser);

  res.status(201).json({
    message: 'User registered successfully',
    user: {
      id: newUser.id,
      name: newUser.name,
      email: newUser.email,
      role: newUser.role,
    },
  });
});

// POST /api/auth/login
router.post('/login', async (req, res) =&gt; {
  const { email, password } = req.body;

  if (!email || !password) {
    return res.status(400).json({ message: 'Email and password are required' });
  }

  const user = findUserByEmail(email);
  if (!user) {
    return res.status(401).json({ message: 'Invalid credentials' });
  }

  const isMatch = await bcrypt.compare(password, user.password);
  if (!isMatch) {
    return res.status(401).json({ message: 'Invalid credentials' });
  }

  // Issue JWT — embed role in the payload
  const token = jwt.sign(
    {
      id: user.id,
      email: user.email,
      role: user.role,   // ← This is the key part for RBAC
    },
    process.env.JWT_SECRET,
    { expiresIn: '24h' }
  );

  res.json({
    message: 'Login successful',
    token,
  });
});

module.exports = router;
</code></pre>
<p>The most important line is the JWT payload:</p>
<pre><code class="language-javascript">jwt.sign({ id, email, role }, process.env.JWT_SECRET, { expiresIn: '24h' })
</code></pre>
<p>By embedding <code>role</code> in the token, every subsequent request carries the user's permissions without requiring a database lookup. The server just verifies the token and reads the role from the payload.</p>
<h2 id="heading-building-the-rbac-middleware">Building the RBAC Middleware</h2>
<p>This is the core of the system. We need two separate middleware functions:</p>
<ol>
<li><p><code>verifyToken</code> confirms the JWT is valid and attaches the decoded payload to <code>req.user</code></p>
</li>
<li><p><code>checkRole</code> confirms the user has the required role for a specific route</p>
</li>
</ol>
<p>Keeping them separate gives you flexibility. Some routes only need authentication. Others need both authentication and a specific role.</p>
<p>Create <code>src/middleware/auth.js</code>:</p>
<pre><code class="language-javascript">const jwt = require('jsonwebtoken');

// Middleware 1: Verify the JWT token
const verifyToken = (req, res, next) =&gt; {
  const authHeader = req.headers['authorization'];
  const token = authHeader &amp;&amp; authHeader.split(' ')[1]; // Expects: Bearer &lt;token&gt;

  if (!token) {
    return res.status(401).json({ message: 'Access denied. No token provided.' });
  }

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded; // Attach decoded payload (including role) to request
    next();
  } catch (err) {
    return res.status(403).json({ message: 'Invalid or expired token.' });
  }
};

// Middleware 2: Check if user has one of the required roles
const checkRole = (...allowedRoles) =&gt; {
  return (req, res, next) =&gt; {
    if (!req.user) {
      return res.status(401).json({ message: 'Not authenticated.' });
    }

    if (!allowedRoles.includes(req.user.role)) {
      return res.status(403).json({
        message: `Access denied. Required role: ${allowedRoles.join(' or ')}. Your role: ${req.user.role}`,
      });
    }

    next();
  };
};

module.exports = { verifyToken, checkRole };
</code></pre>
<p><code>checkRole</code> uses a rest parameter (<code>...allowedRoles</code>) so you can pass in one or multiple roles:</p>
<pre><code class="language-javascript">checkRole('admin')                  // only admin
checkRole('editor', 'admin')        // editor or admin
checkRole('user', 'editor', 'admin') // all roles
</code></pre>
<p>This makes route definitions clean and readable — the permissions are visible right at the route level.</p>
<h2 id="heading-building-the-protected-routes">Building the Protected Routes</h2>
<p>Now let's wire up routes that use the middleware.</p>
<p>Create <code>src/routes/content.js</code>:</p>
<pre><code class="language-javascript">const express = require('express');
const { verifyToken, checkRole } = require('../middleware/auth');

const router = express.Router();

// In-memory content store
const content = [
  { id: '1', title: 'Getting Started with Node.js', author: 'admin' },
  { id: '2', title: 'Express Middleware Explained', author: 'editor' },
];

// GET /api/content — all authenticated users
router.get('/', verifyToken, checkRole('user', 'editor', 'admin'), (req, res) =&gt; {
  res.json({ content });
});

// POST /api/content — editors and admins only
router.post('/', verifyToken, checkRole('editor', 'admin'), (req, res) =&gt; {
  const { title } = req.body;

  if (!title) {
    return res.status(400).json({ message: 'Title is required' });
  }

  const newItem = {
    id: Date.now().toString(),
    title,
    author: req.user.email,
  };

  content.push(newItem);
  res.status(201).json({ message: 'Content created', item: newItem });
});

// DELETE /api/content/:id — admin only
router.delete('/:id', verifyToken, checkRole('admin'), (req, res) =&gt; {
  const index = content.findIndex((c) =&gt; c.id === req.params.id);

  if (index === -1) {
    return res.status(404).json({ message: 'Content not found' });
  }

  content.splice(index, 1);
  res.json({ message: 'Content deleted successfully' });
});

module.exports = router;
</code></pre>
<p>Notice how readable each route is:</p>
<pre><code class="language-javascript">router.delete('/:id', verifyToken, checkRole('admin'), handler)
</code></pre>
<p>You can understand the access control without reading the handler body. This is one of the key advantages of middleware-based RBAC: permissions live at the routing layer, not buried in business logic.</p>
<p>Create <code>src/routes/admin.js</code>:</p>
<pre><code class="language-javascript">const express = require('express');
const { verifyToken, checkRole } = require('../middleware/auth');
const { getAllUsers } = require('../data/users');

const router = express.Router();

// GET /api/admin/users — admin only
router.get('/users', verifyToken, checkRole('admin'), (req, res) =&gt; {
  res.json({ users: getAllUsers() });
});

module.exports = router;
</code></pre>
<h2 id="heading-putting-it-all-together">Putting It All Together</h2>
<p>Create <code>src/app.js</code>:</p>
<pre><code class="language-javascript">require('dotenv').config();
const express = require('express');

const authRoutes = require('./routes/auth');
const contentRoutes = require('./routes/content');
const adminRoutes = require('./routes/admin');

const app = express();

app.use(express.json());

// Routes
app.use('/api/auth', authRoutes);
app.use('/api/content', contentRoutes);
app.use('/api/admin', adminRoutes);

// Health check
app.get('/', (req, res) =&gt; {
  res.json({ message: 'RBAC API is running' });
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () =&gt; {
  console.log(`Server running on port ${PORT}`);
});
</code></pre>
<h2 id="heading-testing-the-api">Testing the API</h2>
<p>Start the server:</p>
<pre><code class="language-bash">npm run dev
</code></pre>
<h3 id="heading-step-1-register-users-with-different-roles">Step 1: Register Users with Different Roles</h3>
<p>Register an admin:</p>
<pre><code class="language-bash">curl -X POST http://localhost:3000/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{"name": "Admin User", "email": "admin@example.com", "password": "password123", "role": "admin"}'
</code></pre>
<p>Register an editor:</p>
<pre><code class="language-bash">curl -X POST http://localhost:3000/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{"name": "Editor User", "email": "editor@example.com", "password": "password123", "role": "editor"}'
</code></pre>
<p>Register a regular user (no role specified — defaults to <code>user</code>):</p>
<pre><code class="language-bash">curl -X POST http://localhost:3000/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{"name": "Regular User", "email": "user@example.com", "password": "password123"}'
</code></pre>
<h3 id="heading-step-2-log-in-and-get-a-token">Step 2: Log in and Get a Token</h3>
<pre><code class="language-bash">curl -X POST http://localhost:3000/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email": "user@example.com", "password": "password123"}'
</code></pre>
<p>You'll get a response like:</p>
<pre><code class="language-json">{
  "message": "Login successful",
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
</code></pre>
<p>Copy the token.</p>
<h3 id="heading-step-3-test-role-based-access">Step 3: Test Role-based Access</h3>
<p><strong>Read content as a regular user (should succeed):</strong></p>
<pre><code class="language-bash">curl http://localhost:3000/api/content \
  -H "Authorization: Bearer YOUR_TOKEN_HERE"
</code></pre>
<p><strong>Try creating content as a regular user (should fail — 403):</strong></p>
<pre><code class="language-bash">curl -X POST http://localhost:3000/api/content \
  -H "Authorization: Bearer YOUR_TOKEN_HERE" \
  -H "Content-Type: application/json" \
  -d '{"title": "New Article"}'
</code></pre>
<p>Response:</p>
<pre><code class="language-json">{
  "message": "Access denied. Required role: editor or admin. Your role: user"
}
</code></pre>
<p>Now log in as an editor and try the same POST request. It succeeds. Log in as admin and try the DELETE route. Only the admin token will work.</p>
<h3 id="heading-step-4-decode-the-jwt-to-see-the-role">Step 4: Decode the JWT to See the Role</h3>
<p>You can paste any token into <a href="https://jwt.io">jwt.io</a> to inspect the payload. You'll see something like:</p>
<pre><code class="language-json">{
  "id": "1720300000000",
  "email": "admin@example.com",
  "role": "admin",
  "iat": 1720300000,
  "exp": 1720386400
}
</code></pre>
<p>The <code>role</code> field is exactly what <code>checkRole</code> reads on every protected request.</p>
<h2 id="heading-key-takeaways">Key Takeaways</h2>
<p>Roles live in the JWT payload. The role travels with the token — no extra DB call needed every time someone hits a protected route. It gets embedded at login and verified cryptographically on each request.</p>
<p>Middleware is composable. <code>verifyToken</code> and <code>checkRole</code> are separate, reusable functions. You can chain them on any route in any combination.</p>
<p>Permissions are visible at the route level. <code>router.delete('/:id', verifyToken, checkRole('admin'), handler)</code> tells you everything about access control before you even read the handler.</p>
<p><strong>Before you ship this to production:</strong></p>
<ul>
<li><p>The in-memory array was just to keep this tutorial focused — replace it with a real database before anything goes near production. A server restart wipes all your users right now.</p>
</li>
<li><p>That 24h token expiry is too long. Cut it to 15 minutes and add refresh token rotation. A stolen token becomes useless fast.</p>
</li>
<li><p>Re-validate roles from the DB on sensitive operations. A role change won't reflect in an existing token until it expires</p>
</li>
<li><p>HTTPS, always</p>
</li>
<li><p>If your permission logic grows beyond "check a role", look at <a href="https://casl.js.org/">casl</a>. It handles attribute-level rules cleanly</p>
</li>
</ul>
<h2 id="heading-conclusion">Conclusion</h2>
<p>The core of it fits in two middleware functions and a JWT payload. I've used this same pattern across several projects. And once you've built it yourself, you'll start spotting it everywhere, because almost every multi-user app needs some version of it.</p>
<p><strong>Full source code on GitHub:</strong> <a href="https://github.com/ziaongit/nodejs-rbac-jwt-api">github.com/ziaongit/nodejs-rbac-jwt-api</a></p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Browser-Based PDF OCR to Text Converter Using JavaScript ]]>
                </title>
                <description>
                    <![CDATA[ Not every PDF contains searchable or editable text. Many PDFs are simply scanned images of documents such as invoices, contracts, books, receipts, government forms, and handwritten notes. While these  ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-pdf-ocr-to-text-converter-javascript/</link>
                <guid isPermaLink="false">6a4d27fa9720ba8235700935</guid>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ pdf ]]>
                    </category>
                
                    <category>
                        <![CDATA[ OCR  ]]>
                    </category>
                
                    <category>
                        <![CDATA[ pdf to text ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Bhavin Sheth ]]>
                </dc:creator>
                <pubDate>Tue, 07 Jul 2026 16:23:22 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/ba3a97e6-1829-4062-acef-9d05eaa14c34.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Not every PDF contains searchable or editable text. Many PDFs are simply scanned images of documents such as invoices, contracts, books, receipts, government forms, and handwritten notes.</p>
<p>While these documents are easy to read, copying, searching, or editing their content isn't possible without additional processing.</p>
<p>This is where <strong>Optical Character Recognition (OCR)</strong> comes in. OCR recognizes text inside scanned images and converts it into editable, searchable digital text.</p>
<p>In this tutorial, you'll build a browser-based <strong>PDF OCR to Text Converter</strong> using JavaScript. Users will be able to upload PDF files, preview pages, configure OCR settings, extract text, monitor processing progress, review OCR confidence scores, and export the results – all directly inside the browser.</p>
<p>Since everything runs locally, uploaded documents never leave the user's device, making the tool both fast and privacy-friendly.</p>
<p>By the end of this tutorial, you'll understand how browser-based OCR works and how to build your own PDF-to-text converter using JavaScript.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-why-pdf-ocr-is-useful">Why PDF OCR Is Useful</a></p>
</li>
<li><p><a href="#heading-how-pdf-ocr-works">How PDF OCR 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-creating-the-upload-interface">Creating the Upload Interface</a></p>
</li>
<li><p><a href="#heading-previewing-uploaded-pdf-pages">Previewing Uploaded PDF Pages</a></p>
</li>
<li><p><a href="#heading-configuring-ocr-settings">Configuring OCR Settings</a></p>
</li>
<li><p><a href="#heading-extracting-text-from-the-pdf">Extracting Text from the PDF</a></p>
</li>
<li><p><a href="#heading-tracking-ocr-progress">Tracking OCR Progress</a></p>
</li>
<li><p><a href="#heading-understanding-ocr-confidence-scores">Understanding OCR Confidence Scores</a></p>
</li>
<li><p><a href="#heading-reviewing-the-extracted-text">Reviewing the Extracted Text</a></p>
</li>
<li><p><a href="#heading-exporting-the-ocr-results">Exporting the OCR Results</a></p>
</li>
<li><p><a href="#heading-demo-how-the-pdf-ocr-tool-works">Demo: How the PDF OCR Tool Works</a></p>
</li>
<li><p><a href="#heading-performance-optimization-tips">Performance Optimization Tips</a></p>
</li>
<li><p><a href="#heading-important-notes-from-real-world-use">Important Notes from Real-World Use</a></p>
</li>
<li><p><a href="#heading-common-mistakes-to-avoid">Common Mistakes to Avoid</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-why-pdf-ocr-is-useful">Why PDF OCR Is Useful</h2>
<p>Many PDF files are scanned documents rather than digital text. Although they look readable, the text is actually stored as images, making it impossible to search, copy, edit, or analyze the content.</p>
<p>OCR (Optical Character Recognition) solves this problem by recognizing characters from scanned pages and converting them into editable, searchable text. Once the text is extracted, it can be copied, translated, indexed, summarized, or imported into other applications.</p>
<p>OCR is widely used across many industries. Businesses use it to process invoices, purchase orders, receipts, contracts, bank statements, and tax documents without manually entering data. Legal professionals use OCR to search agreements, affidavits, and court documents for names, dates, or specific clauses. Government agencies digitize historical records, application forms, passports, and official documents to build searchable digital archives.</p>
<p>Educational institutions convert scanned books, research papers, lecture notes, and examination materials into searchable text, making learning resources easier to access. Healthcare organizations use OCR to digitize prescriptions, laboratory reports, insurance claims, and patient records, reducing paperwork and improving record management.</p>
<p>OCR is also valuable for e-commerce businesses. Sellers handling hundreds of invoices, shipping labels, and purchase orders from platforms such as Amazon, Flipkart, Meesho, or Shopify can quickly extract order numbers, customer details, addresses, and product information instead of typing everything manually.</p>
<p>Developers use OCR when building document management systems, enterprise search tools, AI assistants, and workflow automation platforms where scanned documents need to become searchable digital content.</p>
<p>Since this application performs OCR entirely inside the browser, users can process confidential documents without uploading them to external servers. This keeps document processing fast, private, and secure while making scanned PDFs much more useful.</p>
<h2 id="heading-how-pdf-ocr-works">How PDF OCR Works</h2>
<p>A PDF OCR application converts scanned pages into editable text by combining PDF rendering with Optical Character Recognition.</p>
<p>When a user uploads a PDF, the browser first validates the document and loads it into memory. Each page is then rendered as an image using PDF.js. These rendered page images become the input for the OCR engine.</p>
<p>The OCR engine examines every image pixel by pixel. It identifies printed characters, recognizes words and sentences, and reconstructs the document as digital text. Depending on the selected language, the recognition engine applies language-specific dictionaries and character models to improve accuracy.</p>
<p>If the user enables image enhancement, the application can improve the scanned page before recognition. Converting the page to grayscale, increasing contrast, or sharpening the image often helps OCR detect characters more accurately, especially when working with old scans or low-quality photocopies.</p>
<p>As each page is processed, the application updates a progress indicator so users can monitor the extraction process in real time. The OCR engine also returns a confidence score for every page, allowing users to estimate how reliable the recognized text is.</p>
<p>After all selected pages have been processed, the application combines the extracted text into a single document. Users can review the output, copy it directly from the browser, or export it as a TXT or JSON file for further use.</p>
<p>Since every stage of the workflow runs locally, the uploaded PDF never leaves the user's device. This makes browser-based OCR an excellent solution for sensitive business documents, legal records, healthcare files, financial reports, and government paperwork.</p>
<h2 id="heading-project-setup">Project Setup</h2>
<p>We'll build the PDF OCR application using standard web technologies.</p>
<p>Create the following project structure.</p>
<pre><code class="language-text">pdf-ocr-tool/

│── index.html

│── style.css

│── script.js
</code></pre>
<p>Next, include the required JavaScript libraries inside <strong>index.html</strong>.</p>
<pre><code class="language-html">&lt;script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.4.168/pdf.min.js"&gt;&lt;/script&gt;

&lt;script src="https://cdn.jsdelivr.net/npm/tesseract.js@5/dist/tesseract.min.js"&gt;&lt;/script&gt;

&lt;script src="https://unpkg.com/pdf-lib"&gt;&lt;/script&gt;
</code></pre>
<p>These libraries provide everything needed to render PDF pages, recognize text, and manage PDF-related operations directly inside the browser.</p>
<h2 id="heading-what-libraries-are-we-using">What Libraries Are We Using?</h2>
<p>This project combines several JavaScript libraries because OCR involves multiple processing stages.</p>
<p>The primary library is <strong>PDF.js</strong>, which loads the uploaded PDF document and renders every page as an image inside the browser. Since OCR engines work with images rather than PDF files directly, rendering each page is the first step of the workflow.</p>
<p>The application uses <strong>Tesseract.js</strong> to perform Optical Character Recognition. Tesseract is one of the most popular open-source OCR engines and supports dozens of languages, making it possible to recognize printed text from scanned documents without relying on any external API or cloud service.</p>
<p>We also include <strong>PDF-lib</strong>, which helps manage PDF-related operations and provides additional flexibility if future features such as annotations, metadata editing, or document modifications are added.</p>
<p>Together, these libraries create a complete browser-based OCR solution capable of rendering PDF pages, recognizing printed text, tracking recognition progress, reporting confidence scores, and exporting the extracted text while keeping every document private on the user's device.</p>
<h2 id="heading-creating-the-upload-interface">Creating the Upload Interface</h2>
<p>Every OCR workflow begins with selecting a PDF document. Before the application can recognize any text, it must first load the PDF into the browser and verify that it's a supported file type.</p>
<p>A good upload interface should be simple, intuitive, and accessible for both desktop and mobile users. Supporting drag-and-drop uploads alongside the traditional file picker gives users multiple ways to import their documents.</p>
<p>In this project, the upload section serves as the starting point for the entire OCR workflow. After a PDF is selected, the browser validates the file, reads it into memory, and prepares it for page rendering. Since the application runs completely inside the browser, no document is uploaded to an external server. This ensures confidential PDFs remain private throughout the OCR process.</p>
<p>The upload interface also provides clear instructions so users immediately understand how to begin using the tool.</p>
<p>Here's the HTML for the upload area:</p>
<pre><code class="language-html">&lt;div class="upload-container"&gt;

    &lt;div id="dropZone" class="drop-zone"&gt;

        &lt;div class="upload-icon"&gt;
            ☁
        &lt;/div&gt;

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

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

        &lt;button id="selectPDF"&gt;

            Select PDF

        &lt;/button&gt;

        &lt;input

            type="file"

            id="pdfInput"

            accept="application/pdf"

            hidden&gt;

    &lt;/div&gt;

&lt;/div&gt;
</code></pre>
<p>Next, validate the uploaded file before loading it.</p>
<pre><code class="language-javascript">const pdfInput = document.getElementById("pdfInput");

pdfInput.addEventListener("change", async (event)=&gt;{

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

    if(!file) return;

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

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

        return;

    }

    loadPDF(file);

});
</code></pre>
<p>Once the validation succeeds, the PDF is loaded into memory and the application proceeds to generate preview thumbnails for each page.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/c47b97f2-6e5f-421d-90c6-0dd34e3440ed.png" alt="PDF upload interface allowing users to drag and drop or browse for a PDF document before OCR processing." style="display:block;margin:0 auto" width="570" height="636" loading="lazy">

<h2 id="heading-previewing-uploaded-pdf-pages">Previewing Uploaded PDF Pages</h2>
<p>After the PDF has been loaded successfully, the application generates page previews.</p>
<p>Instead of immediately starting OCR, users first see thumbnail images for every page in the uploaded document. This allows them to confirm that the correct file has been selected and inspect the document before extraction begins.</p>
<p>The preview stage is especially useful for large PDFs because users can quickly identify scanned pages, blank pages, rotated pages, or incorrect uploads without wasting time running OCR on the wrong document.</p>
<p>PDF.js renders every page as a canvas before displaying it inside the preview grid.</p>
<p>First, load the PDF document.</p>
<pre><code class="language-javascript">const pdf = await pdfjsLib.getDocument({

    data: await file.arrayBuffer()

}).promise;
</code></pre>
<p>Next, render every page.</p>
<pre><code class="language-javascript">for(let pageNumber = 1; pageNumber &lt;= pdf.numPages; pageNumber++){

    const page = await pdf.getPage(pageNumber);

    const viewport = page.getViewport({

        scale:0.35

    });

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

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

    canvas.width = viewport.width;

    canvas.height = viewport.height;

    await page.render({

        canvasContext:context,

        viewport

    }).promise;

    previewContainer.appendChild(canvas);

}
</code></pre>
<p>Each rendered page becomes a thumbnail, allowing users to scroll through the document before choosing the OCR settings.</p>
<p>This visual confirmation greatly reduces mistakes when processing long reports, contracts, invoices, books, or multi-page scanned documents.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/84da8fa8-372f-47b9-ad16-fef208211722.png" alt="Uploaded PDF preview displaying thumbnail images of every page before OCR processing begins." style="display:block;margin:0 auto" width="564" height="505" loading="lazy">

<h2 id="heading-configuring-ocr-settings">Configuring OCR Settings</h2>
<p>Different PDF documents require different OCR configurations. A clean digital scan usually processes very quickly, while old photocopies or low-quality scans often require additional image enhancement to improve recognition accuracy.</p>
<p>Before starting OCR, the application allows users to customize several options that affect how text is extracted.</p>
<p>Users can choose whether OCR should process every page or only a specific page range. This is particularly useful when working with large documents where only a few pages contain important information.</p>
<p>The OCR engine also supports multiple recognition languages. Selecting the correct language helps improve accuracy because Tesseract uses language-specific dictionaries and character models during recognition.</p>
<p>For users who prioritize speed, the Fast mode completes OCR quickly while still producing good results. When working with low-quality scans or official documents, High Accuracy mode performs additional processing to improve recognition quality.</p>
<p>The application also includes optional image enhancement settings. Converting pages to grayscale, increasing contrast, or sharpening the scanned image often improves OCR accuracy by making printed characters easier to recognize.</p>
<p>These configurable options allow the OCR engine to adapt to many different document types without overwhelming users with unnecessary complexity.</p>
<p>The page selection section allows users to process either the entire document or only selected pages.</p>
<pre><code class="language-html">&lt;input

type="radio"

name="pages"

value="all"

checked&gt;

All Pages

&lt;input

type="radio"

name="pages"

value="custom"&gt;

Specific Pages
</code></pre>
<p>Users can also choose the OCR language.</p>
<pre><code class="language-html">&lt;select id="language"&gt;

    &lt;option&gt;English&lt;/option&gt;

    &lt;option&gt;Hindi&lt;/option&gt;

    &lt;option&gt;Gujarati&lt;/option&gt;

    &lt;option&gt;Spanish&lt;/option&gt;

    &lt;option&gt;French&lt;/option&gt;

    &lt;option&gt;German&lt;/option&gt;

    &lt;option&gt;Chinese (Simplified)&lt;/option&gt;

&lt;/select&gt;
</code></pre>
<p>Next, configure the OCR accuracy mode.</p>
<pre><code class="language-javascript">const mode = document.querySelector(

'input[name="accuracy"]:checked'

).value;

console.log(mode);
</code></pre>
<p>Finally, enable optional image enhancement features before OCR begins.</p>
<pre><code class="language-javascript">const grayscale = grayscaleCheckbox.checked;

const contrast = contrastCheckbox.checked;

const sharpen = sharpenCheckbox.checked;

console.log(

grayscale,

contrast,

sharpen

);
</code></pre>
<p>These settings allow the application to balance processing speed and recognition quality depending on the type of PDF being analyzed.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/9e4287b5-bb0c-4ac3-a042-5e99ec37be07.png" alt="OCR settings showing page selection, language selection, accuracy mode, and image enhancement options." style="display:block;margin:0 auto" width="565" height="563" loading="lazy">

<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/0335cedb-8dcd-4b8a-b26a-58e71b7f056f.png" alt="Language selection dropdown displaying supported OCR languages including English, Hindi, Gujarati, Spanish, French, German, and Chinese." style="display:block;margin:0 auto" width="526" height="292" loading="lazy">

<h3 id="heading-improving-ocr-accuracy-before-processing">Improving OCR Accuracy Before Processing</h3>
<p>One advantage of browser-based OCR is that the document can be optimized before recognition begins. Small image enhancements often have a significant impact on the quality of the extracted text.</p>
<p>For example, grayscale conversion removes unnecessary color information, allowing the OCR engine to focus only on character shapes. Increasing contrast helps distinguish text from the page background, while sharpening makes blurred letters easier to recognize.</p>
<p>These enhancements are especially valuable when processing old books, photocopies, historical records, receipts, handwritten forms, government documents, engineering drawings, and low-resolution scans.</p>
<p>Choosing the correct OCR language is equally important. A scanned Gujarati document processed using the English language model will usually produce poor recognition results. Selecting the matching language significantly improves OCR accuracy.</p>
<p>Taking a few moments to configure these settings before processing often produces cleaner extracted text, fewer recognition errors, and higher confidence scores, particularly when working with challenging documents.</p>
<h2 id="heading-extracting-text-from-the-pdf">Extracting Text from the PDF</h2>
<p>Once the document has been uploaded, previewed, and the OCR settings have been configured, the application is ready to extract text from the selected pages.</p>
<p>Unlike searchable PDFs that already contain digital text, scanned PDF documents consist entirely of images. OCR works by examining each rendered page image, recognizing every visible character, and converting those characters into editable text.</p>
<p>The extraction process begins by rendering each selected PDF page as an image using PDF.js. Each rendered page is then passed to Tesseract.js, which analyzes the image pixel by pixel and reconstructs words, sentences, paragraphs, and punctuation.</p>
<p>If the user selected a specific page range, only those pages are processed. Otherwise, every page in the document is analyzed.</p>
<p>Because OCR can be computationally intensive, especially for high-resolution scans, the application processes one page at a time. This approach keeps memory usage lower while providing continuous progress updates to the user.</p>
<p>The recognized text from each page is appended to a single output document that can later be reviewed, copied, or exported.</p>
<p>First, create the OCR worker.</p>
<pre><code class="language-javascript">const worker = await Tesseract.createWorker(

    selectedLanguage

);
</code></pre>
<p>Next, loop through the selected pages.</p>
<pre><code class="language-javascript">for(let page = startPage; page &lt;= endPage; page++){

    await processPage(page);

}
</code></pre>
<p>Now perform OCR on the rendered page.</p>
<pre><code class="language-javascript">const result = await worker.recognize(

    canvas

);

const extractedText = result.data.text;
</code></pre>
<p>Append the extracted text to the final output.</p>
<pre><code class="language-javascript">finalText +=

`----- Page ${page} -----\n\n`;

finalText += extractedText;

finalText += "\n\n";
</code></pre>
<p>Once every page has been processed, terminate the OCR worker.</p>
<pre><code class="language-javascript">await worker.terminate();
</code></pre>
<p>Processing one page at a time allows users to monitor OCR progress while ensuring stable performance, even for large documents.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/f0d63a41-e872-40c3-9dd1-6bec86a8a545.png" alt="Extract Text button used to begin OCR processing for the uploaded PDF." style="display:block;margin:0 auto" width="575" height="171" loading="lazy">

<h2 id="heading-tracking-ocr-progress">Tracking OCR Progress</h2>
<p>OCR processing can take anywhere from a few seconds to several minutes depending on the size of the document, image quality, language, and selected accuracy mode.</p>
<p>Providing a progress indicator is important because users can immediately see that the application is actively processing the document instead of appearing frozen.</p>
<p>As each page finishes recognition, the progress bar updates automatically, displaying both the current page number and the overall completion percentage.</p>
<p>For example, a 42-page document may display messages such as "Processing Page 2 of 42" before eventually reaching the final page.</p>
<p>Showing real-time progress improves the overall user experience and makes it easier to estimate the remaining processing time.</p>
<p>The OCR engine reports its progress while recognizing each page.</p>
<pre><code class="language-javascript">logger: info =&gt; {

    console.log(info);

}
</code></pre>
<p>Update the progress bar.</p>
<pre><code class="language-javascript">progressBar.style.width =

`${percentage}%`;

progressLabel.innerText =

`${percentage}%`;
</code></pre>
<p>Display the currently processed page.</p>
<pre><code class="language-javascript">status.innerText =

`Processing Page ${currentPage}

of ${totalPages}`;
</code></pre>
<p>Once the final page has been processed, the progress bar reaches one hundred percent and the extracted text becomes available for review.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/bc5e38e8-9fb8-44c8-8a8e-1dcd0c44cc5c.png" alt="OCR progress indicator showing the current page being processed and the overall completion percentage." style="display:block;margin:0 auto" width="567" height="100" loading="lazy">

<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/b00a2478-9079-49db-b8e7-909985109016.png" alt="OCR progress reaching the final page before completing text extraction." style="display:block;margin:0 auto" width="559" height="94" loading="lazy">

<h2 id="heading-understanding-ocr-confidence-scores">Understanding OCR Confidence Scores</h2>
<p>One useful feature of Tesseract.js is that it reports a confidence score for every page that it processes.</p>
<p>The confidence score estimates how accurately the OCR engine recognized the characters contained on a page. Higher confidence generally indicates cleaner scans, sharper text, and fewer recognition errors.</p>
<p>For example, a professionally scanned document with clear printed text may produce confidence scores above ninety-five percent, while older photocopies or blurry mobile phone images may produce lower values.</p>
<p>Displaying confidence scores helps users quickly identify pages that may require manual review or reprocessing.</p>
<p>In this application, every processed page displays its individual OCR confidence score after recognition finishes.</p>
<p>The OCR engine returns the confidence value together with the extracted text.</p>
<pre><code class="language-javascript">const confidence =

result.data.confidence;
</code></pre>
<p>Store each page's score.</p>
<pre><code class="language-javascript">confidenceScores.push({

    page: currentPage,

    confidence

});
</code></pre>
<p>Display the results.</p>
<pre><code class="language-javascript">confidenceScores.forEach(score=&gt;{

    console.log(

        score.page,

        score.confidence

    );

});
</code></pre>
<p>Pages with lower confidence scores may contain faded text, handwritten notes, poor lighting, skewed scans, or low image resolution. Reviewing these pages helps improve the overall quality of the extracted document.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/b08a9ace-3e1b-474c-9f13-939ed55522b5.png" alt="OCR confidence scores displayed for every processed PDF page." style="display:block;margin:0 auto" width="160" height="697" loading="lazy">

<h2 id="heading-optimizing-ocr-accuracy">Optimizing OCR Accuracy</h2>
<p>Even with a powerful OCR engine, the quality of the original document has a significant impact on the extracted text.</p>
<p>Scanned PDFs with sharp, high-resolution pages usually produce excellent results without additional processing. But documents containing faded printing, uneven lighting, shadows, handwritten annotations, or compression artifacts may require image enhancement before OCR begins.</p>
<p>The application includes several preprocessing options that improve recognition quality.</p>
<p>Grayscale conversion removes unnecessary color information and simplifies the image for the OCR engine. Increasing contrast helps separate text from the background, while sharpening improves character edges that may appear blurry in low-quality scans.</p>
<p>Selecting the correct recognition language is equally important. OCR models are trained for specific languages, so choosing the matching language greatly improves character recognition and reduces spelling mistakes.</p>
<p>Users should also select the appropriate accuracy mode. Fast Mode works well for clean digital scans, while High Accuracy Mode performs additional analysis that produces better results for difficult documents, although it requires more processing time.</p>
<p>Taking a few extra seconds to configure these settings often produces significantly cleaner text, higher confidence scores, and fewer manual corrections after extraction.</p>
<h2 id="heading-reviewing-the-extracted-text">Reviewing the Extracted Text</h2>
<p>Once the OCR process finishes, the application combines the recognized text from every processed page into a single output area.</p>
<p>Instead of immediately downloading the results, users can first review the extracted text directly inside the browser. This provides an opportunity to verify the OCR output, check formatting, identify recognition errors, and ensure that the correct pages were processed.</p>
<p>The extracted text preserves the page sequence by separating the content from each page with a clear page heading. This makes it much easier to navigate large documents such as books, contracts, technical manuals, invoices, government records, and research papers.</p>
<p>For searchable PDFs, the extracted text is usually very accurate. For scanned documents, users can quickly compare the OCR output with the original page preview and decide whether additional image enhancement or a different OCR language would improve the results.</p>
<p>The application also includes a <strong>Copy</strong> button so users can instantly copy all extracted text to the clipboard without downloading a file.</p>
<p>First, display the extracted text.</p>
<pre><code class="language-javascript">document.getElementById(

"output"

).value = finalText;
</code></pre>
<p>Next, implement the copy feature.</p>
<pre><code class="language-javascript">async function copyText(){

    await navigator.clipboard.writeText(

        finalText

    );

    alert("Text copied successfully.");

}
</code></pre>
<p>Attach the event listener.</p>
<pre><code class="language-javascript">document.getElementById(

"copyButton"

).addEventListener(

"click",

copyText

);
</code></pre>
<p>Providing an in-browser preview allows users to verify OCR quality before exporting the results.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/97a5b7c6-834f-4fd9-baba-c5f50b35708a.png" alt="Extracted OCR text displayed inside the browser with a copy button for quickly copying the recognized text." style="display:block;margin:0 auto" width="551" height="266" loading="lazy">

<h2 id="heading-exporting-the-ocr-results">Exporting the OCR Results</h2>
<p>After reviewing the extracted content, users can export it in different formats depending on how they intend to use the information.</p>
<p>Plain text files are ideal for editing inside any text editor, importing into word processors, or searching with desktop applications.</p>
<p>JSON exports are useful for developers building document management systems, AI applications, search engines, automation workflows, or APIs that consume structured OCR results.</p>
<p>Providing multiple export formats makes the OCR tool suitable for both everyday users and software developers.</p>
<p>Creating a downloadable TXT file is straightforward.</p>
<pre><code class="language-javascript">const blob = new Blob(

    [finalText],

    {

        type:"text/plain"

    }

);
</code></pre>
<p>Generate the download link.</p>
<pre><code class="language-javascript">const url = URL.createObjectURL(

blob

);

const link = document.createElement(

"a"

);

link.href = url;

link.download = "ocr-output.txt";

link.click();
</code></pre>
<p>JSON exports include additional information such as page numbers and confidence scores.</p>
<pre><code class="language-javascript">const report = {

    text: finalText,

    confidence: confidenceScores

};

downloadJSON(report);
</code></pre>
<p>These export options allow users to continue working with the extracted text in virtually any application.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/2dc04fb4-b1ff-498b-876d-6eeb85e59de9.png" alt="Export options allowing users to download OCR results as TXT or JSON files." style="display:block;margin:0 auto" width="575" height="123" loading="lazy">

<h2 id="heading-demo-how-the-pdf-ocr-tool-works">Demo: How the PDF OCR Tool Works</h2>
<h3 id="heading-step-1-upload-your-pdf">Step 1: Upload Your PDF</h3>
<p>The OCR workflow begins by uploading a PDF document using either the drag-and-drop area or the file picker.</p>
<p>Once a document has been selected, the browser validates the file format, loads the PDF into memory, and prepares it for page rendering. Since all processing occurs locally, the uploaded file never leaves the user's computer.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/eb91d9de-30de-480b-93cd-70f2ae0ca5e7.png" alt="PDF upload interface allowing users to select a PDF document for OCR text extraction." style="display:block;margin:0 auto" width="570" height="636" loading="lazy">

<h3 id="heading-step-2-preview-the-uploaded-pdf">Step 2: Preview the Uploaded PDF</h3>
<p>After the upload is complete, the application renders thumbnail previews of every page.</p>
<p>This allows users to verify that the correct document has been selected and inspect the page order before running OCR.</p>
<p>Previewing the document is particularly useful when processing large books, reports, legal documents, or scanned archives containing dozens of pages.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/622cfe19-938c-46af-aa40-0cee319ee477.png" alt="Uploaded PDF preview displaying page thumbnails before OCR processing begins." style="display:block;margin:0 auto" width="564" height="505" loading="lazy">

<h3 id="heading-step-3-configure-ocr-settings">Step 3: Configure OCR Settings</h3>
<p>Before text extraction begins, users configure the OCR options.</p>
<p>The application allows users to choose all pages or a specific page range, select the OCR language, switch between Fast and High Accuracy modes, and enable optional image enhancement features such as grayscale conversion, contrast improvement, and sharpening.</p>
<p>These settings help improve recognition quality depending on the condition of the scanned document.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/6bedc5cd-cc83-4bc6-be6f-2e7da57e408d.png" alt="OCR configuration panel showing page selection, language selection, accuracy mode, and image enhancement options.language selection menu displaying multiple supported recognition languages." style="display:block;margin:0 auto" width="565" height="563" loading="lazy">

<h3 id="heading-step-4-start-ocr-processing">Step 4: Start OCR Processing</h3>
<p>After reviewing the settings, users click the <strong>Extract Text</strong> button.</p>
<p>The browser begins processing every selected page one by one. During this stage, each rendered page image is analyzed by the OCR engine, which recognizes printed characters and converts them into editable text.</p>
<p>Because OCR runs directly inside the browser, even confidential documents remain completely private throughout the process.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/9f9cf7cd-6cf8-4686-ab7c-d4b5ab96dc47.png" alt="Extract Text button used to begin OCR processing for the uploaded PDF." style="display:block;margin:0 auto" width="575" height="171" loading="lazy">

<h3 id="heading-step-5-monitor-processing-progress">Step 5: Monitor Processing Progress</h3>
<p>As OCR runs, the application displays a live progress indicator.</p>
<p>Users can monitor the current page being processed, overall completion percentage, and recognition progress in real time. For large documents, this provides useful feedback and reassures users that the application is actively processing the file.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/51a2fcc1-f97f-4e9f-b53d-4a133b0ec4fc.png" alt="OCR progress indicator displaying the current page and completion percentage" style="display:block;margin:0 auto" width="567" height="100" loading="lazy">

<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/1db2c8f7-84f7-441b-9cbe-2a691e98c40f.png" alt="OCR processing nearing completion on the final page of the document." style="display:block;margin:0 auto" width="575" height="133" loading="lazy">

<h3 id="heading-step-6-review-ocr-confidence-scores">Step 6: Review OCR Confidence Scores</h3>
<p>Once recognition is complete, the application displays confidence scores for every processed page.</p>
<p>These values indicate how accurately the OCR engine recognized each page. Pages with lower confidence scores may contain faded text, skewed scans, or poor image quality and can be reviewed manually if necessary.</p>
<p>Confidence scores provide an additional layer of quality assurance before exporting the extracted text.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/1a3fec10-8c8c-4104-b9f9-9a555029999c.png" alt="OCR confidence scores displayed for each processed PDF page." style="display:block;margin:0 auto" width="160" height="697" loading="lazy">

<h3 id="heading-step-7-review-the-extracted-text">Step 7: Review the Extracted Text</h3>
<p>After OCR finishes, the complete extracted text appears inside the browser.</p>
<p>Users can scroll through the recognized content, compare it with the original document, and copy the text directly to the clipboard using the built-in Copy button.</p>
<p>This makes it easy to reuse the extracted information immediately without downloading a separate file.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/52b22a16-9fa3-4b34-9687-7923be187f4f.png" alt="Browser-based OCR text output with a built-in copy button." style="display:block;margin:0 auto" width="551" height="266" loading="lazy">

<h3 id="heading-step-8-export-the-results">Step 8: Export the Results</h3>
<p>Finally, users can export the OCR results.</p>
<p>The application supports downloading the extracted text as a TXT file for general editing or as a JSON file for software development and automation workflows.</p>
<p>After selecting the preferred format, the browser generates the file instantly without uploading any data to external servers.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/e60a2073-a432-4b97-a879-83430abb9cdb.png" alt="Export section allowing users to download OCR results in TXT or JSON format." style="display:block;margin:0 auto" width="575" height="123" loading="lazy">

<h2 id="heading-performance-optimization-tips">Performance Optimization Tips</h2>
<p>OCR is one of the most computationally intensive operations performed inside a browser. Although modern JavaScript engines and OCR libraries are highly optimized, a few simple techniques can significantly improve performance.</p>
<p>Before processing begins, render PDF pages at an appropriate resolution. Extremely high-resolution images increase processing time without always improving recognition accuracy.</p>
<pre><code class="language-javascript">const viewport = page.getViewport({

    scale:1.5

});
</code></pre>
<p>Processing pages sequentially instead of loading every page simultaneously reduces memory consumption for large documents.</p>
<pre><code class="language-javascript">for(let page = 1; page &lt;= totalPages; page++){

    await processPage(page);

}
</code></pre>
<p>Users should enable OCR only when working with scanned PDFs. Searchable PDFs already contain digital text, so OCR simply increases processing time without improving the results.</p>
<p>If the document contains hundreds of pages, allowing users to analyze only a selected page range can significantly reduce processing time.</p>
<p>Using grayscale images instead of full-color pages also improves recognition speed while reducing memory usage.</p>
<p>Whenever possible, choose the OCR language that matches the document. Smaller language models generally process faster and produce more accurate results than attempting recognition with an incorrect language.</p>
<p>Finally, remember to terminate the OCR worker after processing completes to release browser resources.</p>
<pre><code class="language-javascript">await worker.terminate();
</code></pre>
<p>These small optimizations produce a smoother user experience while making browser-based OCR practical even for large documents.</p>
<h2 id="heading-important-notes-from-real-world-use">Important Notes from Real-World Use</h2>
<p>OCR accuracy depends heavily on the quality of the original document.</p>
<p>Clean scans with high resolution, good lighting, and sharp printed text usually produce excellent recognition results. Older photocopies, faded documents, handwritten notes, or skewed scans may require image enhancement before OCR begins.</p>
<p>Before processing, always verify that the uploaded file is a valid PDF.</p>
<pre><code class="language-javascript">if(file.type !== "application/pdf"){

    alert("Please upload a valid PDF.");

    return;

}
</code></pre>
<p>Selecting the correct OCR language is equally important. Processing a Gujarati document with the English language model will significantly reduce recognition accuracy.</p>
<pre><code class="language-javascript">console.log(

"Selected Language:",

selectedLanguage

);
</code></pre>
<p>Users should also review OCR confidence scores after processing. Pages with lower confidence values often benefit from rescanning or using image enhancement options.</p>
<p>Because the entire workflow runs locally, browser-based OCR is well suited for confidential business reports, contracts, financial documents, legal records, healthcare files, and government paperwork that should never be uploaded to third-party services.</p>
<h2 id="heading-common-mistakes-to-avoid">Common Mistakes to Avoid</h2>
<p>One common mistake is enabling OCR for documents that already contain selectable text.</p>
<p>Searchable PDFs can usually be processed much faster by extracting the embedded text directly.</p>
<pre><code class="language-javascript">if(pdfHasText){

    skipOCR();

}
</code></pre>
<p>Another mistake is choosing the wrong recognition language.</p>
<p>Always select the language that matches the document before starting OCR.</p>
<pre><code class="language-javascript">worker = await Tesseract.createWorker(

selectedLanguage

);
</code></pre>
<p>Some users also attempt OCR on extremely low-quality scans without enabling image enhancement.</p>
<p>Using grayscale conversion, contrast adjustment, or sharpening often improves recognition quality considerably.</p>
<p>Finally, always review the extracted text before exporting it.</p>
<p>Checking the OCR output and confidence scores helps identify pages that may require rescanning or additional processing before the results are used in business workflows.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this tutorial, you built a browser-based PDF OCR to Text Converter using JavaScript.</p>
<p>You learned how to upload PDF documents, preview scanned pages, configure OCR settings, select recognition languages, improve image quality, extract text, monitor processing progress, review OCR confidence scores, and export the recognized text directly from the browser.</p>
<p>More importantly, you discovered how modern browsers can perform Optical Character Recognition locally without requiring a backend server or cloud-based OCR service.</p>
<p>This approach keeps document processing fast, private, and secure while giving users complete control over how scanned PDFs are converted into editable text.</p>
<p>You can try the complete implementation here:</p>
<p><a href="https://allinonetools.net/pdf-to-text/"><strong>PDF OCR to Text Converter</strong></a></p>
<p>Once you understand this workflow, you can extend the project further by adding handwriting recognition, AI-powered document summarization, automatic translation, named entity extraction, keyword detection, document classification, searchable PDF generation, or intelligent document automation.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Browser-Based PDF Analyzer Using JavaScript ]]>
                </title>
                <description>
                    <![CDATA[ PDF files are one of the most widely used document formats for sharing reports, invoices, contracts, books, research papers, manuals, forms, and business documents. Although viewing a PDF is simple, u ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-pdf-analyzer-javascript/</link>
                <guid isPermaLink="false">6a47d7568dc454430aaca51a</guid>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ pdf ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Tutorial ]]>
                    </category>
                
                    <category>
                        <![CDATA[ freeCodeCamp.org ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Bhavin Sheth ]]>
                </dc:creator>
                <pubDate>Fri, 03 Jul 2026 15:37:58 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/a009f906-4ae4-4808-99fa-a31b419f66d5.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>PDF files are one of the most widely used document formats for sharing reports, invoices, contracts, books, research papers, manuals, forms, and business documents. Although viewing a PDF is simple, understanding what's inside the document is often much more difficult.</p>
<p>For example, you may need to know how many pages a PDF contains, whether it's password protected, who created it, what metadata it includes, how much text it contains, which fonts are used, or whether the document contains embedded images.</p>
<p>Manually inspecting all of this information can be time-consuming, especially when working with large collections of PDF files.</p>
<p>A PDF Analyzer solves this problem by automatically extracting detailed information from a document. Instead of opening the file in multiple applications, users can upload a PDF once and instantly view metadata, security settings, text statistics, image information, page details, fonts, and much more.</p>
<p>In this tutorial, you'll build a browser-based PDF Analyzer using JavaScript. The application allows users to upload a PDF, preview its pages, configure analysis options, perform different levels of document analysis, inspect the extracted information, and export a complete analysis report in multiple formats.</p>
<p>Everything runs directly inside the browser without requiring a backend server, making document analysis fast, private, and secure.</p>
<p>By the end of this tutorial, you'll have a fully functional PDF Analyzer capable of examining both simple and complex PDF documents.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/ba3b5025-7320-422d-a0ca-c96858c3ea73.png" alt="allinonetools pdf tools pdf analyzer tool" style="display:block;margin:0 auto" width="574" height="277" loading="lazy">

<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-why-pdf-analysis-is-useful">Why PDF Analysis Is Useful</a></p>
</li>
<li><p><a href="#heading-how-pdf-analysis-works">How PDF Analysis Works</a></p>
</li>
<li><p><a href="#heading-project-setup">Project Setup</a></p>
</li>
<li><p><a href="#heading-what-library-are-we-using">What Library Are We Using?</a></p>
</li>
<li><p><a href="#heading-creating-the-upload-interface">Creating the Upload Interface</a></p>
</li>
<li><p><a href="#heading-previewing-uploaded-pdf-pages">Previewing Uploaded PDF Pages</a></p>
</li>
<li><p><a href="#heading-configuring-analysis-settings">Configuring Analysis Settings</a></p>
</li>
<li><p><a href="#heading-analyzing-the-pdf">Analyzing the PDF</a></p>
</li>
<li><p><a href="#heading-displaying-the-analysis-report">Displaying the Analysis Report</a></p>
</li>
<li><p><a href="#heading-exporting-the-analysis-report">Exporting the Analysis Report</a></p>
</li>
<li><p><a href="#heading-demo-how-the-pdf-analyzer-works">Demo: How the PDF Analyzer Works</a></p>
</li>
<li><p><a href="#heading-important-notes-from-real-world-use">Important Notes from Real-World Use</a></p>
</li>
<li><p><a href="#heading-common-mistakes-to-avoid">Common Mistakes to Avoid</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-why-pdf-analysis-is-useful">Why PDF Analysis Is Useful</h2>
<p>Most people think of a PDF as simply a document that can be viewed or printed, but every PDF contains much more information than what appears on the screen.</p>
<p>Behind every document is a collection of properties such as metadata, security settings, page information, fonts, embedded images, and document statistics. Accessing this information can help users better understand the document before editing, sharing, printing, or archiving it.</p>
<p>Businesses often receive hundreds of PDF files every day from clients, suppliers, government departments, and employees. Before these files are stored or distributed, they frequently need to be inspected to verify their contents. A PDF Analyzer makes this process much faster by automatically extracting important document information.</p>
<p>Legal professionals regularly review contracts and agreements where document properties such as creation dates, authorship, and security restrictions may be important. Instead of manually checking each document, an analyzer provides these details in seconds.</p>
<p>Educational institutions use PDF analysis when reviewing assignments, research papers, and digital course materials. Teachers and administrators can quickly inspect page counts, metadata, extracted text, and document properties before storing or distributing files.</p>
<p>Publishing companies analyze PDF files before printing books, manuals, catalogs, and magazines. Reviewing page sizes, fonts, metadata, and embedded resources helps identify formatting problems before production begins.</p>
<p>Government agencies and healthcare organizations also benefit from document analysis when processing applications, medical records, permits, forms, and official reports. Verifying document integrity before long-term storage helps reduce errors and maintain consistent records.</p>
<p>A PDF Analyzer is equally useful for developers. Before building editing tools such as watermarking, page rotation, cropping, metadata editing, or page extraction, developers often need to inspect the document structure to determine how it should be processed.</p>
<p>Because this application performs all analysis directly inside the browser, users can inspect sensitive documents without uploading them to external servers. This provides an additional layer of privacy while delivering instant results.</p>
<h2 id="heading-how-pdf-analysis-works">How PDF Analysis Works</h2>
<p>A PDF Analyzer reads the uploaded document and extracts useful information from its internal structure.</p>
<p>Once the user selects a PDF file, the browser loads the document into memory. Instead of modifying the PDF, the application examines its contents and collects various types of information that can later be displayed in a structured report.</p>
<p>The analysis begins by reading the document itself. Basic properties such as the filename, total number of pages, and file size are identified immediately.</p>
<p>Next, the application extracts metadata including the document title, author, subject, keywords, creator, producer, creation date, modification date, and PDF version.</p>
<p>The analyzer can also inspect security-related properties to determine whether the document is password protected or contains restrictions on printing, copying, or editing.</p>
<p>After processing the document structure, the application examines each page individually. It can count words, characters, images, fonts, estimate reading time, calculate speaking time, and even perform sentiment analysis on extracted text when OCR is enabled.</p>
<p>If the uploaded document consists of scanned pages instead of selectable text, OCR can be used to recognize text before analysis begins.</p>
<p>Once all information has been collected, the application generates a complete report that can be viewed inside the browser or exported as a PDF, JSON, CSV, or text file.</p>
<p>Since the entire workflow runs locally, the original document remains on the user's device throughout the process.</p>
<h2 id="heading-project-setup">Project Setup</h2>
<p>We'll build this project using standard web technologies.</p>
<p>Create the following files:</p>
<pre><code class="language-text">pdf-analyzer/

│── index.html

│── style.css

│── script.js
</code></pre>
<p>Next, include the required libraries inside <strong>index.html</strong>.</p>
<pre><code class="language-html">&lt;script src="https://unpkg.com/pdf-lib"&gt;&lt;/script&gt;

&lt;script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.4.168/pdf.min.js"&gt;&lt;/script&gt;

&lt;script src="https://cdn.jsdelivr.net/npm/tesseract.js@5/dist/tesseract.min.js"&gt;&lt;/script&gt;

&lt;script src="https://cdn.jsdelivr.net/npm/chart.js"&gt;&lt;/script&gt;
</code></pre>
<p>These libraries provide everything needed for PDF loading, rendering, OCR processing, and report visualization.</p>
<h2 id="heading-what-library-are-we-using">What Library Are We Using?</h2>
<p>This project combines several JavaScript libraries because no single library can perform every type of PDF analysis.</p>
<p>The primary library is <strong>PDF-lib</strong>, which allows the application to load PDF documents and access important document properties such as metadata and page information. It's lightweight, fast, and runs entirely inside modern browsers.</p>
<p>The project also uses <strong>PDF.js</strong> to render document pages for previews. This enables users to visually inspect uploaded PDFs before running the analysis.</p>
<p>For scanned documents that don't contain selectable text, <strong>Tesseract.js</strong> provides Optical Character Recognition (OCR). It recognizes text directly inside the browser, making it possible to analyze scanned PDFs without requiring any server-side processing.</p>
<p>To visualize analysis results, we'll use <strong>Chart.js</strong> for generating simple graphs and statistics such as word counts, sentiment distribution, and other document metrics.</p>
<p>Together, these libraries create a powerful browser-based PDF Analyzer capable of extracting metadata, rendering previews, recognizing scanned text, generating statistics, and exporting detailed analysis reports while keeping every document completely private.</p>
<h2 id="heading-creating-the-upload-interface">Creating the Upload Interface</h2>
<p>Every PDF workflow begins with selecting a document. Before any analysis can take place, users need a simple and reliable way to upload one or more PDF files into the browser.</p>
<p>A good upload interface should clearly indicate that only PDF documents are accepted while supporting both drag-and-drop uploads and the traditional file picker. This makes the tool easy to use regardless of whether users are working on a desktop or a mobile device.</p>
<p>In this project, the upload area acts as the entry point for the entire analysis process. When a user selects a PDF, the browser validates the file type, loads the document into memory, and prepares it for previewing and analysis. Since everything happens locally, the original PDF never leaves the user's device.</p>
<p>Our upload component displays a drag-and-drop area, a browse button, and helpful instructions that guide users through the first step of the workflow.</p>
<p>Here's the HTML for the upload area:</p>
<pre><code class="language-html">&lt;div class="upload-container"&gt;

    &lt;div id="dropZone" class="drop-zone"&gt;

        &lt;div class="upload-icon"&gt;
            ☁
        &lt;/div&gt;

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

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

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

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

    &lt;/div&gt;

&lt;/div&gt;
</code></pre>
<p>Next, register the file input and handle PDF selection.</p>
<pre><code class="language-javascript">const pdfInput = document.getElementById("pdfInput");

pdfInput.addEventListener("change", async (event) =&gt; {

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

    if (!file) return;

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

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

        return;

    }

    loadPDF(file);

});
</code></pre>
<p>This validation prevents unsupported file types from being processed while ensuring the application only loads valid PDF documents.</p>
<p>After the upload interface is complete, users can immediately select a document and move to the preview stage.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/5a12d42f-494d-434f-8717-66b7563c6c52.png" alt="PDF upload interface allowing users to drag and drop or browse for a PDF document before analysis." style="display:block;margin:0 auto" width="740" height="548" loading="lazy">

<h2 id="heading-previewing-uploaded-pdf-pages">Previewing Uploaded PDF Pages</h2>
<p>Once a PDF has been uploaded, it's helpful to display a visual preview before starting the analysis. This allows users to verify that they selected the correct document and quickly inspect its pages.</p>
<p>Instead of showing only the file name, our application renders thumbnail previews of every page in the PDF. Users can scroll through the thumbnails to inspect the document and confirm that all pages loaded successfully.</p>
<p>Displaying previews also improves the user experience because it gives immediate visual feedback while the document is being prepared for analysis.</p>
<p>The browser uses PDF.js to render each page as a canvas before converting it into an image that can be displayed inside the page preview grid.</p>
<p>The following code loads the PDF document:</p>
<pre><code class="language-javascript">const pdf = await pdfjsLib.getDocument({

    data: await file.arrayBuffer()

}).promise;
</code></pre>
<p>Next, render each page:</p>
<pre><code class="language-javascript">for (let pageNumber = 1; pageNumber &lt;= pdf.numPages; pageNumber++) {

    const page = await pdf.getPage(pageNumber);

    const viewport = page.getViewport({

        scale: 0.35

    });

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

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

    canvas.width = viewport.width;

    canvas.height = viewport.height;

    await page.render({

        canvasContext: context,

        viewport

    }).promise;

    previewContainer.appendChild(canvas);

}
</code></pre>
<p>Each page is rendered independently, making it possible to preview documents containing dozens or even hundreds of pages.</p>
<p>The preview shown in this project displays all page thumbnails together, making it easy to verify page order before continuing.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/f74f0c85-6910-445f-b005-710c312a6281.png" alt="Uploaded PDF preview displaying page thumbnails before document analysis begins." style="display:block;margin:0 auto" width="745" height="705" loading="lazy">

<h2 id="heading-configuring-analysis-settings">Configuring Analysis Settings</h2>
<p>Before analyzing the document, users can customize how the application should examine the PDF.</p>
<p>Different documents require different levels of analysis. Some users may only need basic information such as the page count and metadata, while others may want detailed statistics about extracted text, embedded images, fonts, security permissions, and OCR results.</p>
<p>To support these different scenarios, the PDF Analyzer provides several configurable options before processing begins.</p>
<p>The first option allows users to choose which pages should be analyzed. They can analyze every page in the document or specify a custom page range when only certain pages are relevant.</p>
<p>For scanned PDFs, OCR can be enabled to recognize text that's stored as images rather than selectable characters. Users can also select the OCR language before processing starts.</p>
<p>Finally, the application offers multiple analysis levels. Basic mode extracts essential document information such as metadata and security properties. Standard mode additionally collects text and image statistics. Advanced mode performs the most detailed inspection available, including fonts, page-level statistics, OCR processing, and sentiment analysis.</p>
<p>The analysis settings panel gives users complete control over how the document should be processed while keeping the interface simple and easy to understand.</p>
<p>Here's the HTML used for the settings panel:</p>
<pre><code class="language-html">&lt;select id="analysisLevel"&gt;

    &lt;option value="basic"&gt;
        Basic (Info, Metadata, Security)
    &lt;/option&gt;

    &lt;option value="standard"&gt;
        Standard (Basic + Text &amp; Image Stats)
    &lt;/option&gt;

    &lt;option value="advanced"&gt;
        Advanced (All Features)
    &lt;/option&gt;

&lt;/select&gt;
</code></pre>
<p>Users can also enable OCR when analyzing scanned PDF documents:</p>
<pre><code class="language-javascript">const enableOCR = document.getElementById("enableOCR").checked;

const language = document.getElementById("ocrLanguage").value;

if (enableOCR) {

    console.log("OCR Enabled");

    console.log(language);

}
</code></pre>
<p>Finally, capture the selected analysis level:</p>
<pre><code class="language-javascript">const level = document.getElementById("analysisLevel").value;

switch (level) {

    case "basic":

        runBasicAnalysis();

        break;

    case "standard":

        runStandardAnalysis();

        break;

    case "advanced":

        runAdvancedAnalysis();

        break;

}
</code></pre>
<p>These settings allow the application to adapt to many different types of PDF documents, from simple text files to complex scanned reports containing images, metadata, and security restrictions.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/62bf34b8-e706-44b3-a2b9-7f6481ed98ff.png" alt="PDF analysis settings showing page selection, OCR configuration, language selection, and available analysis levels." style="display:block;margin:0 auto" width="741" height="703" loading="lazy">

<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/886c865b-fa4a-42aa-95d2-9b507cfbe43b.png" alt="OCR language selection dropdown with multiple supported languages." style="display:block;margin:0 auto" width="731" height="254" loading="lazy">

<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/39370da7-2caf-4536-9603-03562b6206ce.png" alt="Analysis level selector showing Basic, Standard, and Advanced PDF analysis modes." style="display:block;margin:0 auto" width="670" height="174" loading="lazy">

<h2 id="heading-analyzing-the-pdf">Analyzing the PDF</h2>
<p>Once the PDF has been uploaded, previewed, and the analysis options have been configured, the application is ready to examine the document.</p>
<p>Unlike editing tools that modify pages, a PDF Analyzer inspects the document and extracts useful information without changing the original file. The analyzer reads the PDF structure, examines each page, and collects information that can later be displayed in a detailed report.</p>
<p>The analysis begins by loading the uploaded document into memory. From there, the application extracts basic information such as the filename, file size, total number of pages, and document validity. It then reads metadata including the title, author, subject, creator, producer, creation date, modification date, and PDF version.</p>
<p>Depending on the selected analysis level, the application can also inspect security permissions, count words and characters, estimate reading time, identify embedded images, list fonts used throughout the document, and perform OCR on scanned PDFs. When OCR is enabled, the analyzer converts scanned images into searchable text before calculating document statistics.</p>
<p>Because the application processes everything inside the browser, users receive instant results while maintaining complete privacy.</p>
<p>The first step is loading the uploaded PDF:</p>
<pre><code class="language-javascript">async function analyzePDF(file){

    const bytes = await file.arrayBuffer();

    const pdf = await PDFLib.PDFDocument.load(bytes);

    return pdf;

}
</code></pre>
<p>Next, extract the document metadata:</p>
<pre><code class="language-javascript">const metadata = {

    title: pdf.getTitle(),

    author: pdf.getAuthor(),

    subject: pdf.getSubject(),

    creator: pdf.getCreator(),

    producer: pdf.getProducer(),

    keywords: pdf.getKeywords(),

    creationDate: pdf.getCreationDate(),

    modificationDate: pdf.getModificationDate()

};
</code></pre>
<p>Basic document information is also collected:</p>
<pre><code class="language-javascript">const fileInfo = {

    fileName: file.name,

    fileSize: file.size,

    totalPages: pdf.getPageCount(),

    valid: true

};
</code></pre>
<p>If the user selects Advanced Analysis, additional routines extract page statistics, fonts, images, OCR results, and text analysis:</p>
<pre><code class="language-javascript">if(selectedLevel === "advanced"){

    analyzeFonts();

    analyzeImages();

    analyzeText();

    performOCR();

}
</code></pre>
<p>Once every analysis step has finished, the application combines the collected information into a single report object that will be displayed in the next stage.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/f5fbb355-dabb-4536-ae09-d2c3e79c2c4c.png" alt="Analyze PDF button used to generate a complete PDF analysis report." style="display:block;margin:0 auto" width="399" height="70" loading="lazy">

<h2 id="heading-displaying-the-analysis-report">Displaying the Analysis Report</h2>
<p>After processing is complete, the application presents the collected information inside a structured report.</p>
<p>Instead of showing raw JSON or technical output, the report organizes related information into separate cards. This layout makes it much easier for users to understand large amounts of document information.</p>
<p>The first section displays basic document information, including the filename, file size, total number of pages, and validation status.</p>
<p>The metadata section contains properties such as the document title, author, subject, keywords, creator, producer, PDF version, creation date, and modification date.</p>
<p>Security information indicates whether the document is password protected and whether printing, copying, or modification restrictions are present.</p>
<p>When text analysis is enabled, the report includes the total word count, character count, average words per page, estimated reading time, and estimated speaking time. If OCR has been performed, the extracted text is also analyzed to calculate sentiment statistics.</p>
<p>Additional cards display image information, embedded fonts, and page-by-page extracted text for users who need a deeper inspection of the document.</p>
<p>The following example creates a simple report section:</p>
<pre><code class="language-javascript">function renderBasicInfo(info){

    document.getElementById("fileName").textContent = info.fileName;

    document.getElementById("pageCount").textContent = info.totalPages;

    document.getElementById("fileSize").textContent = info.fileSize;

}
</code></pre>
<p>Rendering the metadata is straightforward:</p>
<pre><code class="language-javascript">function renderMetadata(metadata){

    title.innerText = metadata.title;

    author.innerText = metadata.author;

    creator.innerText = metadata.creator;

    producer.innerText = metadata.producer;

}
</code></pre>
<p>Page-wise extracted content can also be displayed:</p>
<pre><code class="language-javascript">pages.forEach((page,index)=&gt;{

    createPageCard(

        index + 1,

        page.text

    );

});
</code></pre>
<p>Organizing the results into individual sections allows users to quickly locate the information they need without scrolling through large blocks of text.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/08b89c29-48ee-4fd2-a5c1-059c2b61e732.png" alt="PDF analysis report displaying metadata, security information, text statistics, image information, fonts, and document insights." style="display:block;margin:0 auto" width="674" height="715" loading="lazy">

<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/b84c18f4-5cfc-4ea5-82c6-c4bf1b45495d.png" alt="Page-wise extracted text generated during PDF document analysis." style="display:block;margin:0 auto" width="660" height="880" loading="lazy">

<h2 id="heading-exporting-the-analysis-report">Exporting the Analysis Report</h2>
<p>After reviewing the analysis results, users often need to save the report for future reference or share it with colleagues.</p>
<p>To support different workflows, the PDF Analyzer allows the report to be exported in several formats. Depending on the user's needs, the report can be downloaded as a PDF document, JSON file, CSV spreadsheet, or plain text file.</p>
<p>PDF reports are useful for documentation and sharing with clients or team members. JSON exports are ideal for developers who want to process the analysis programmatically. CSV files can be opened in spreadsheet applications for further analysis, while text files provide a simple human-readable version of the report.</p>
<p>Providing multiple export formats makes the analyzer suitable for business users, developers, researchers, and system administrators alike.</p>
<p>The following example creates a JSON export:</p>
<pre><code class="language-javascript">const report = JSON.stringify(

    analysisResult,

    null,

    2

);
</code></pre>
<p>Create a downloadable file:</p>
<pre><code class="language-javascript">const blob = new Blob(

    [report],

    {

        type:"application/json"

    }

);
</code></pre>
<p>Generate the download link:</p>
<pre><code class="language-javascript">const url = URL.createObjectURL(blob);

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

link.href = url;

link.download = "analysis-report.json";

link.click();
</code></pre>
<p>The export menu allows users to choose the most appropriate output format before downloading the completed report.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/bed0c18f-4e25-492c-88c8-24423ce0f6b1.png" alt="bed0c18f-4e25-492c-88c8-24423ce0f6b1" style="display:block;margin:0 auto" width="901" height="373" loading="lazy">

<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/39ce70f7-d2dd-4c5a-9fe1-95eeb0c0b0ba.png" alt="Export format dropdown allowing users to select PDF, JSON, CSV, or text output before downloading." style="display:block;margin:0 auto" width="631" height="201" loading="lazy">

<h2 id="heading-demo-how-the-pdf-analyzer-works">Demo: How the PDF Analyzer Works</h2>
<h3 id="heading-step-1-upload-your-pdf-file">Step 1: Upload Your PDF File</h3>
<p>The process begins by uploading a PDF document using either the drag-and-drop area or the file selection button.</p>
<p>Once a file is selected, the browser validates that it's a PDF before loading it into memory. Because the application runs entirely inside the browser, the uploaded document never leaves the user's device, making the tool suitable for confidential business reports, contracts, invoices, research papers, legal documents, and other sensitive files.</p>
<p>After the PDF is loaded successfully, the application prepares it for page preview generation and document analysis.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/cce8e552-7d7d-4ec0-ac98-0312ae9b2395.png" alt="PDF upload interface allowing users to drag and drop or browse for a PDF document before analysis." style="display:block;margin:0 auto" width="740" height="548" loading="lazy">

<h3 id="heading-step-2-preview-uploaded-pdf-pages">Step 2: Preview Uploaded PDF Pages</h3>
<p>After the document has been loaded, the application generates page previews for the uploaded PDF.</p>
<p>Displaying page thumbnails allows users to confirm that the correct file has been selected before analysis begins. Users can quickly browse through the document, inspect page order, and verify that every page has loaded successfully.</p>
<p>This visual preview also helps identify scanned pages, blank pages, or unexpected formatting issues before processing.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/80d61017-4222-44a4-8c0b-71a17e9fa3aa.png" alt="Uploaded PDF page thumbnails displayed before document analysis." style="display:block;margin:0 auto" width="745" height="705" loading="lazy">

<h3 id="heading-step-3-configure-analysis-settings">Step 3: Configure Analysis Settings</h3>
<p>Next, users configure how the PDF should be analyzed.</p>
<p>The tool allows users to choose whether every page or only a specific page range should be processed. For scanned PDFs, OCR can be enabled to recognize text stored as images, and users can select the appropriate recognition language.</p>
<p>The application also offers multiple analysis levels. Basic mode extracts essential document properties, Standard mode adds text and image statistics, and Advanced mode performs a more detailed inspection that includes fonts, OCR, page-level information, sentiment analysis, and additional document insights.</p>
<p>These settings allow users to customize the analysis based on the type of PDF they are working with.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/c6b089c7-006e-4661-8890-f093bea294e8.png" alt="PDF analysis settings showing page selection, OCR configuration, language selection, and analysis level options." style="display:block;margin:0 auto" width="741" height="703" loading="lazy">

<h3 id="heading-step-4-analyze-the-pdf">Step 4: Analyze the PDF</h3>
<p>Once the settings have been reviewed, users simply click the <strong>Analyze PDF</strong> button.</p>
<p>The browser reads the uploaded document and extracts the selected information. Depending on the chosen analysis level, the application examines metadata, security settings, page information, extracted text, fonts, embedded images, and OCR results.</p>
<p>Although large documents may require a few additional seconds, the entire analysis is completed locally without uploading the PDF to a remote server.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/8eda0352-95ba-4735-914d-3b95ff975f30.png" alt="Analyze PDF button used to generate the document analysis report." style="display:block;margin:0 auto" width="399" height="70" loading="lazy">

<h3 id="heading-step-5-review-the-analysis-report">Step 5: Review the Analysis Report</h3>
<p>After processing is complete, the application displays a comprehensive analysis report.</p>
<p>The report is divided into multiple sections that make it easy to inspect different aspects of the document. Users can review basic document information, metadata, security settings, extracted text statistics, page information, fonts, embedded images, OCR results, estimated reading time, speaking time, and sentiment analysis.</p>
<p>Each section is organized into individual cards so that important information can be located quickly.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/850b54a3-74a5-4617-bdf3-faac236a0eee.png" alt="PDF analysis report displaying metadata, security settings, text statistics, fonts, images, and document insights." style="display:block;margin:0 auto" width="674" height="715" loading="lazy">

<h3 id="heading-step-6-review-page-level-analysis">Step 6: Review Page-Level Analysis</h3>
<p>For users who need more detailed information, the application also displays page-by-page analysis.</p>
<p>Each page can include extracted text, OCR output, word count, image statistics, page dimensions, and additional information collected during processing.</p>
<p>This level of detail is especially useful when analyzing large reports, scanned books, research papers, contracts, technical documentation, and multi-page business documents.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/dea81704-27d8-44a5-a0bd-8756659fcef2.png" alt="Page-by-page PDF analysis showing extracted content and document statistics." style="display:block;margin:0 auto" width="660" height="880" loading="lazy">

<h3 id="heading-step-7-export-the-analysis-report">Step 7: Export the Analysis Report</h3>
<p>After reviewing the analysis, users can export the report for future reference.</p>
<p>The tool supports multiple export formats, including PDF, JSON, CSV, and plain text. This allows developers, researchers, businesses, and system administrators to choose the format that best fits their workflow.</p>
<p>Exported reports can be archived, shared with team members, imported into other systems, or used for additional processing.</p>
<p>Once the desired format is selected, the browser generates the report and downloads it instantly.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/3b799aaa-8c47-4670-8d5e-77bac599c550.png" alt="Export analysis report section showing download options for PDF, JSON, CSV, and text files." style="display:block;margin:0 auto" width="901" height="373" loading="lazy">

<img src="https://cdn.hashnode.com/uploads/covers/6979d22f93bc273cc33971b1/2f33c51b-d52b-43e6-979b-00eaeafe3162.png" alt="Export format selector allowing users to choose PDF, JSON, CSV, or text output before downloading." style="display:block;margin:0 auto" width="631" height="201" loading="lazy">

<h2 id="heading-important-notes-from-real-world-use">Important Notes from Real-World Use</h2>
<p>A PDF Analyzer can process everything from a single-page document to large reports containing hundreds of pages. While modern browsers handle most documents efficiently, larger files containing high-resolution images or scanned pages may require additional processing time, especially when OCR is enabled.</p>
<p>Before starting the analysis, it's good practice to validate the uploaded file.</p>
<pre><code class="language-javascript">if (file.type !== "application/pdf") {

    alert("Please upload a valid PDF document.");

    return;

}
</code></pre>
<p>If OCR is enabled, remember that recognizing text from scanned pages takes longer than extracting text from a standard searchable PDF. Users should only enable OCR when it's actually needed.</p>
<pre><code class="language-javascript">if(enableOCR){

    console.log("Running OCR Analysis...");

}
</code></pre>
<p>When analyzing very large documents, processing pages individually helps reduce memory usage and keeps the browser responsive.</p>
<pre><code class="language-javascript">for(let page = 1; page &lt;= pdf.numPages; page++){

    analyzePage(page);

}
</code></pre>
<p>Before exporting the report, review the extracted information to ensure metadata, text statistics, page information, and OCR results are accurate.</p>
<h2 id="heading-common-mistakes-to-avoid">Common Mistakes to Avoid</h2>
<p>One common mistake is running OCR on documents that already contain selectable text.</p>
<p>OCR is designed for scanned PDFs where text exists only as images. Running OCR on searchable PDFs increases processing time without improving the analysis.</p>
<pre><code class="language-javascript">if(pdfContainsText){

    enableOCR = false;

}
</code></pre>
<p>Another mistake is selecting the wrong analysis level.</p>
<p>For example, users who only need metadata and document properties can choose <strong>Basic Analysis</strong> instead of <strong>Advanced Analysis</strong>, which performs additional processing such as OCR, font inspection, sentiment analysis, and image detection.</p>
<pre><code class="language-javascript">const analysisLevel = "basic";

console.log(analysisLevel);
</code></pre>
<p>Some users also forget to verify the page selection before starting the analysis.</p>
<p>When working with large reports, analyzing only the required pages can significantly reduce processing time.</p>
<pre><code class="language-javascript">const pageRange = "1-20";

console.log(pageRange);
</code></pre>
<p>Finally, always review the generated report before exporting it.</p>
<p>A quick inspection helps verify that metadata, page statistics, OCR output, document properties, and extracted text are accurate before downloading the final report.</p>
<p>Taking a few extra moments to validate the results can save considerable time when working with large document collections.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this tutorial, you built a browser-based PDF Analyzer using JavaScript.</p>
<p>You learned how to upload PDF files, preview document pages, configure analysis options, inspect metadata, analyze document structure, extract text, perform OCR, generate detailed reports, and export the analysis in multiple formats directly from the browser.</p>
<p>More importantly, you saw how modern browsers can inspect complex PDF documents without requiring a backend server or uploading files to third-party services.</p>
<p>This approach keeps document analysis fast, private, and secure while giving users valuable insights into the contents and structure of their PDF files.</p>
<p>You can try the complete implementation here:</p>
<p><strong>PDF Analyzer:</strong> <a href="https://allinonetools.net/pdf-analyzer/">https://allinonetools.net/pdf-analyzer/</a></p>
<p>Once you understand this workflow, you can extend the project further by adding AI-powered document summarization, keyword extraction, duplicate document detection, document comparison, accessibility analysis, compliance checking, digital signature validation, or advanced reporting dashboards.</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
