India's #1 Free Text-to-Handwriting Conversion Platform β Transforming typed content into photorealistic handwritten documents with multi-language support, intelligent typography simulation, and HD export pipelines.
- Project Links
- Backend Repository Note
- Project Overview
- Core Feature Matrix
- System Architecture
- Tech Stack Deep Dive
- Handwriting Simulation Engine
- Theme & Paper Style System
- Multi-Language Pipeline
- Canvas Rendering Architecture
- PDF Export Pipeline
- Anti-Copy & OCR Obfuscation Layer
- Frontend State Management
- Backend Django Architecture
- Database Schema
- Performance Optimization
- Directory Structure
- Setup & Local Development
- Environment Variables
- Deployment
- CI / CD Pipeline
- Containerization
- Testing Strategy
- Contributing
- Frontend Repository: https://github.com/vk93102/HandtotextAI-Frontend
- Backend Repository: https://github.com/vk93102/HandtotextAI-Backend
- Demo Video: https://drive.google.com/file/d/1wkw9F5njJeNiZ_bltCMBCbArgOQmcg1q/view?usp=sharing
This repository contains Frontend services and .tsx files for the project. Backend code is maintained separately in the frontend repository linked above.
WriteByHand.in is a client-heavy, server-assisted SaaS platform that leverages HTML5 Canvas APIs, WebFont rendering pipelines, and procedural noise algorithms to simulate authentic human handwriting across multiple scripts including Latin (English), Devanagari (Hindi), Marathi, Gujarati, and Sanskrit.
The platform solves a uniquely hard problem: making machine-rendered text indistinguishable from actual handwriting through stochastic character-level perturbations, ink pressure simulation, baseline wobble injection, ligature approximation, and contextual glyph variation β all running at 60fps in the browser with zero server-side rendering latency.
| Goal | Implementation Approach |
|---|---|
| Zero-latency preview | Client-side canvas rendering; no round-trips for live preview |
| Photorealistic output | Multi-layer compositing: ink bleed, paper texture, scanner shadow simulation |
| Multilingual support | Unicode-aware font loading with script-specific glyph metrics |
| Anti-plagiarism evasion | Per-character stochastic transform + OCR interference layer |
| Offline-capable | Service Worker caching of fonts + engine; core tool works without network |
- 9 handwriting fonts including natural-decay fonts (Cedarville, Bad Script) and formal scripts (Mr Dafoe)
- Stochastic variation seed system β reproducible random character-level transforms per document
- Left/Right hand slant simulation via per-glyph affine transform matrices
- Ink pressure modulation β stroke weight variation using Perlin noise sampling
- Baseline wobble β sinusoidal Y-offset per word, modulated by a pseudo-random walk
- 5 paper modes: Ruled, Blank, Graph, Four-Line, CBSE-standard
- Margin presets: Standard and CBSE-compliant (with left punch-hole margin + header zone)
- Dynamic pagination: Overflow detection with automatic page spawning
- Image embedding: Drag-resize image insertion mid-document with text reflow
- 3 quality tiers: WhatsApp (compressed JPEG), Print-Ready (medium DPI), Ultra HD (max DPI)
- PDF multi-page bundling with optional high-compression mode
- PNG per-page export
- Anti-Copy pattern overlay with configurable interference line density
- Smart Q&A mode: Auto-detects
Q:/Ans:prefixes and applies color-coded ink simulation - Auto-headings: ALL CAPS lines rendered in bold black ink
- Auto-correct: Real-time English spell correction pipeline
- Date/Page number headers: Auto-inserted per-page with font-matched rendering
.wbhproject files: Serialized JSON containing document state, settings, images (base64), and variation seed β fully portable and reopenable
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β CLIENT LAYER (Browser) β
β β
β ββββββββββββββββββββββ ββββββββββββββββββββ βββββββββββββββββββ β
β β Editor UI (TS) β β Rendering Engine β β Export Engine β β
β β - React-like VDOM ββββΆβ (Canvas API) ββββΆβ (jsPDF + JPEG) β β
β β - ContentEditable β β - TextLayout β β - Compression β β
β β - Live word count β β - GlyphPainter β β - Multi-page β β
β ββββββββββββββββββββββ β - NoiseEngine β β - Anti-copy β β
β β - TextureLayer β βββββββββββββββββββ β
β ββββββββββββββββββββββ ββββββββββββββββββββ β
β β Settings Panel β β
β β - Font picker β ββββββββββββββββββββ βββββββββββββββββββ β
β β - Page style β β WebFont Loader β β State Manager β β
β β - Margin ctrl ββββΆβ (Google Fonts β β (.wbh project) β β
β β - Realism ctrl β β + local cache) β β - LocalStorage β β
β ββββββββββββββββββββββ ββββββββββββββββββββ βββββββββββββββββββ β
β β
β Service Worker ββββ Font Cache ββββ Offline Engine Cache β
βββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββββ
β HTTPS (Django REST / SEO / Auth)
βββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββββββ
β SERVER LAYER (Django) β
β β
β ββββββββββββββββββ ββββββββββββββββββββ ββββββββββββββββββββββββββ β
β β Django Views β β REST Framework β β Template Engine β β
β β (SSR pages) β β (API endpoints) β β (Jinja2 + SEO meta) β β
β ββββββββββββββββββ ββββββββββββββββββββ ββββββββββββββββββββββββββ β
β β
β ββββββββββββββββββ ββββββββββββββββββββ ββββββββββββββββββββββββββ β
β β Tool Registry β β Content Engine β β Analytics & Logging β β
β β (30+ tools) β β (AI-assist gen) β β (Custom event store) β β
β ββββββββββββββββββ ββββββββββββββββββββ ββββββββββββββββββββββββββ β
β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Data Layer β β
β β PostgreSQL (user data) β Redis (session/cache) β CDN (assets) β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
WriteByHand.in uses a "thick client, thin server" pattern:
- ~95% of computation runs in the browser β text layout, canvas rendering, PDF generation, noise simulation β all happens client-side. This enables zero-cost horizontal scaling and eliminates server-side rendering bottlenecks.
- The Django backend serves as: (1) an SSR layer for SEO-optimized HTML delivery, (2) a tool registry for 30+ utility pages, (3) a potential persistence/auth API layer, and (4) a static asset + CDN orchestrator.
- TypeScript owns the entire interactive rendering pipeline with strict typing across the canvas engine, state manager, font loader, and export pipeline.
| Component | Technology | Purpose |
|---|---|---|
| Web Framework | Django 4.2 | Routing, ORM, admin, middleware |
| API Layer | Django REST Framework | RESTful endpoints for tool configs, content generation |
| Template Engine | Django Templates + Jinja2 | SSR pages with structured SEO metadata |
| Database | PostgreSQL 15 | User sessions, analytics, saved projects |
| Cache / Sessions | Redis 7 | Session store, hot-path caching |
| Task Queue | Celery + Redis | Async jobs (email, heavy PDF ops) |
| Auth | Django Allauth | OAuth (Google) + email-based auth |
| Deployment | Gunicorn + Nginx | WSGI server with reverse proxy |
This is the crown jewel of WriteByHand.in β a multi-stage stochastic rendering pipeline that produces handwriting which passes casual human inspection.
Raw Input Text
β
βΌ
βββββββββββββββββββββββββββββββββββββββ
β TextLayoutEngine β
β β
β 1. Unicode normalization (NFC) β
β 2. Script detection per-word β
β (Latin vs Devanagari vs mixed) β
β 3. Word-level tokenization β
β 4. Line-break computation β
β (font metrics + available width)β
β 5. Smart Q&A token tagging β
β 6. ALL-CAPS heading detection β
ββββββββββββββββββββ¬βββββββββββββββββββ
β Token stream with layout coords
βΌ
Each character gets a unique transform computed from the variation seed + position index, ensuring:
- Reproducibility: same seed β same document every time
- Uniqueness: different seeds β different "handwriting instances"
interface GlyphTransform {
dx: number; // X offset (horizontal jitter, Β±1.5px)
dy: number; // Y offset (baseline wobble, Β±2px)
rotation: number; // Micro-rotation (Β±1.5Β°)
scaleX: number; // Width compression (0.95β1.05)
scaleY: number; // Height variation (0.96β1.04)
inkWeight: number; // Stroke weight multiplier (0.85β1.15)
}
// Computed via seeded LCG (Linear Congruential Generator):
// transform_i = f(seed, charIndex, wordIndex, lineIndex)Ink pressure variation mimics the natural increase/decrease of pen pressure across a stroke:
Word boundary β pressure reset to baseline
β
βββ First letter: 0.9Γ weight (pen just touching paper)
βββ Middle letters: 1.0β1.1Γ weight (full contact)
βββ Last letter: 0.85β0.95Γ weight (pen lifting)
Perlin noise superimposed across the entire line for
macro-level pressure variation (simulates writer fatigue/
natural movement patterns).
Left-hand mode: matrix.rotate(-6Β° to -12Β°) per glyph
Right-hand mode: matrix.rotate(+4Β° to +8Β°) per glyph
Slant varies slightly per word using:
slant_word = base_slant + noise(wordIndex) Γ 1.5Β°
Each line's baseline follows a composite wave:
baseline_y(x) = line_y
+ Aβ Γ sin(Οβx + Οβ) // long wavelength drift
+ Aβ Γ sin(Οβx + Οβ) // medium frequency ripple
+ random_walk(x) // true stochastic walk
Where Aβ β 1.5px, Aβ β 0.5px, random_walk step β Β±0.3px
Post-render compositing applies a blur kernel to simulate ink soaking into paper fiber:
Ink Bleed Amount β controls blur radius (0β3px Gaussian)
Applied via: ctx.filter = `blur(${bleedRadius}px)`
Then composited with: ctx.globalCompositeOperation = 'multiply'
Final per-character size variation:
charWidth *= 1 + noise(seed, i, 0) Γ 0.04 Γ realismLevel
charHeight *= 1 + noise(seed, i, 1) Γ 0.03 Γ realismLevelEach page is a layered canvas composite with these stacked layers (bottom to top):
Layer 0: Base fill (white or transparent)
Layer 1: Paper texture (PNG overlay, multiply blend)
Layer 2: Page rule lines (ruled/graph/four-line patterns)
Layer 3: Margin lines (left red margin, top blue header)
Layer 4: Scanner shadow (radial gradient vignette)
Layer 5: Handwritten text glyphs
Layer 6: Embedded images
Layer 7: Anti-copy interference pattern (if enabled)
| Mode | Line Pattern | Use Case |
|---|---|---|
| Ruled | Horizontal lines, 30px spacing | Standard notebook, essays, letters |
| Blank | No lines | Art, free-form, certificates |
| Graph | 15px Γ 15px grid | Maths, technical diagrams |
| Four-Line | 4-line Montessori pattern | Primary handwriting practice |
| CBSE | Ruled + wider left margin + header box | Indian school assignment format |
A post-processing radial shadow gradient simulates a flatbed scanner:
const gradient = ctx.createRadialGradient(cx, cy, innerR, cx, cy, outerR);
gradient.addColorStop(0, 'rgba(0,0,0,0)');
gradient.addColorStop(0.7, 'rgba(0,0,0,0.02)');
gradient.addColorStop(1, 'rgba(0,0,0,0.12)');
ctx.fillStyle = gradient;
ctx.globalCompositeOperation = 'multiply';Paper textures are pre-baked PNG assets (tileable, 512Γ512) convolved over the canvas using multiply blend mode. Three texture intensities are selectable: None / Medium / Heavy.
| Script | Languages | Font Support | Special Handling |
|---|---|---|---|
| Latin | English, Cursive | All 9 fonts | Auto-correct, ligature hints |
| Devanagari | Hindi, Marathi, Sanskrit | Kalam, Dekko | Half-form detection, matra alignment |
| Gujarati | Gujarati | Kalam (extended) | Unicode range: U+0A80βU+0AFF |
function detectScript(char: string): ScriptType {
const code = char.codePointAt(0)!;
if (code >= 0x0900 && code <= 0x097F) return 'devanagari';
if (code >= 0x0A80 && code <= 0x0AFF) return 'gujarati';
if (code >= 0x0041 && code <= 0x007A) return 'latin';
return 'unknown';
}Mixed-script lines are rendered in segments: each contiguous run of the same script uses its optimal font and glyph metrics.
Devanagari glyph rendering requires special treatment:
- Virama handling (
ΰ₯, U+094D): suppresses top-bar (matra) on preceding consonant - Conjunct consonants: pre-shaped by the font shaper; canvas uses
fillTextwith proper Unicode - Vowel matras: rendered as combining characters; positioned relative to base consonant
- Nukta support: dot-below diacritic for loanword phonemes
The browser's own Unicode/OpenType shaper handles the heavy lifting; WriteByHand wraps this with its perturbation layer applied after shaping but before blitting to canvas.
Each page is an independent <canvas> element. The rendering pipeline uses offscreen canvas for preview (lower DPI) and switches to full-resolution canvas on export.
const PREVIEW_DPI = 96;
const EXPORT_DPI_MAP = {
'whatsapp': 150,
'print': 200,
'ultra_hd': 300,
};1. clearRect() β wipe canvas
2. drawPaperBackground() β fill color
3. drawTexture() β paper PNG, multiply blend
4. drawRuleLines() β based on paper mode
5. drawMargins() β header/left margin lines
6. drawImages() β user-uploaded images
7. [Per token loop]
a. computeGlyphTransform(seed, tokenIndex)
b. ctx.save()
c. applyTransformMatrix(transform)
d. ctx.font = buildFontString(font, size, weight)
e. ctx.fillStyle = inkColor
f. ctx.fillText(char, x, y)
g. ctx.restore()
8. applyScannerEffect() β vignette gradient
9. applyAntiCopy() β if enabled, draw interference lines
When "Performance Mode" is ON, the preview canvas renders at 0.5Γ DPI with simplified wobble (skip Perlin noise, use LUT-based approximation). Export always uses full quality regardless of this setting.
[Canvas Array] (one per page)
β
βΌ
βββββββββββββββββββββββββββββββ
β PDFExporter.ts β
β β
β 1. Determine output DPI β
β 2. For each canvas: β
β a. toDataURL('jpeg', β
β qualityFactor) β
β b. jsPDF.addImage() β
β c. if not last page: β
β jsPDF.addPage() β
β 3. Apply compression tier β
β 4. jsPDF.save('file.pdf') β
βββββββββββββββββββββββββββββββ
| Tier | JPEG Quality | Approx DPI | Typical Size |
|---|---|---|---|
| WhatsApp/Email | 0.65 | 150 | ~0.3β0.8 MB |
| Print Ready | 0.82 | 200 | ~1β3 MB |
| Ultra HD | 0.95 | 300 | ~4β10 MB |
When "High Compression" is toggled, quality is reduced by an additional 0.15 factor and image dimensions are downsampled before encoding.
- Python 3.11+
- Node.js 20+
- PostgreSQL 15+
- Redis 7+
# Clone the repo
git clone https://github.com/yourusername/writebyhand.git
cd writebyhand
# Create virtual environment
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
# Install Python dependencies
pip install -r requirements.txt
# Configure environment
cp .env.example .env
# Edit .env with your DB credentials, secret key, etc.
# Run migrations
python manage.py migrate
# Create superuser
python manage.py createsuperuser
# Load initial tool data
python manage.py loaddata tools/fixtures/tools.json
# Start development server
python manage.py runserver# Install Node dependencies
npm install
# Build TypeScript (watch mode)
npm run dev
# Production build
npm run build
# Type check
npm run typecheckdocker-compose up --build
# App: http://localhost:8000
# Redis: localhost:6379
# Postgres: localhost:5432# Django
SECRET_KEY=your-secret-key-here
DEBUG=False
ALLOWED_HOSTS=writebyhand.in,www.writebyhand.in
# Database
DATABASE_URL=postgres://user:password@localhost:5432/writebyhand
# Redis
REDIS_URL=redis://localhost:6379/0
# CDN
CDN_BASE_URL=https://cdn.writebyhand.in
STATIC_ROOT=/var/www/writebyhand/static/
# Email (for auth)
EMAIL_HOST=smtp.sendgrid.net
EMAIL_HOST_USER=apikey
EMAIL_HOST_PASSWORD=your-sendgrid-key
# Analytics (optional)
GA_MEASUREMENT_ID=G-XXXXXXXXXXInternet
β
βΌ
Cloudflare (CDN + DDoS protection + SSL termination)
β
βΌ
Nginx (reverse proxy, static files, gzip)
β
ββββΊ Gunicorn (4 workers, Django WSGI)
β β
β ββββΊ PostgreSQL (primary)
β ββββΊ Redis (cache + sessions)
β
ββββΊ Static Files β CDN origin bucket
# Collect static files
python manage.py collectstatic --no-input
# Build frontend assets
npm run build
# Run database migrations
python manage.py migrate
# Restart Gunicorn
sudo systemctl restart gunicorn
# Reload Nginx
sudo nginx -s reloadGitHub Actions pipeline is now configured end-to-end.
Workflow file:
- .github/workflows/ci-cd.yml
- Install dependencies
- Run lint checks (
ruffcritical rules) - Validate deploy configuration
- Run Python unit tests
- Run Django smoke check
- Build Docker image in CI
- On push to
masterormain, deployment is triggered on Render using deploy hook (if configured)
RENDER_DEPLOY_HOOK_URL
./tests/ci_pipeline.shContainerization is included for consistent local and CI environments.
- Docker image definition: Dockerfile
- Local multi-service setup: docker-compose.yml
- Build context cleanup: .dockerignore
Run locally with Docker:
docker compose up --buildThis starts:
- Backend at
http://localhost:8000 - PostgreSQL at
localhost:5432 - Redis at
localhost:6379
Current strategy is layered:
- Config and structure tests
- Environment and project shape checks in
tests/test_*.py
- Pipeline tests
tests/ci_pipeline.shruns deploy checks, unit tests, smoke checks
- Endpoint flow scripts
- Shell-based flow checks in
tests/*.shfor key API paths
Planned improvement areas:
- Add mocked service tests (e.g., external APIs, payment webhooks)
- Add focused edge-case tests for document/PDF flows
- Add coverage reporting and thresholds in CI
- Fork the repository
- Create a feature branch:
git checkout -b feature/your-feature-name - TypeScript: ensure
npm run typecheckpasses with zero errors - Python: ensure
flake8andblackchecks pass - Add tests for any new engine behaviour
- Open a Pull Request with a detailed description
- Python:
black+isort+flake8 - TypeScript:
eslint(strict) +prettier - Commits: Conventional Commits format (
feat:,fix:,perf:,docs:)
Β© WriteByHand.in. All rights reserved.
Built with β€οΈ in India Β· Making student life easier, one handwritten page at a time