kokokor
v2.0.4
Published
A lightweight TypeScript library designed to reconstruct paragraphs from OCRed inputs.
Maintainers
Readme
kokokor
A lightweight TypeScript library designed to reconstruct paragraphs from OCRed inputs. It helps format unstructured text with appropriate paragraph breaks, optimizes for readability, and includes advanced poetry detection and layout analysis capabilities.
Features
- Intelligent text line grouping based on vertical proximity and adaptive spacing analysis
- Advanced paragraph reconstruction with vertical gap and line width analysis
- Right-to-left (RTL) text support with coordinate flipping and normalization
- Poetry detection and preservation using multiple heuristics (centering, word density, hemistichs)
- Layout structure recognition including headings (rectangles), footnotes (below horizontal lines)
- Coordinate normalization ensuring consistent results regardless of source document resolution
- Surya OCR integration with format conversion utilities
- Noise filtering to remove OCR artifacts and improve text quality
- Customizable parameters for different document types and languages
- Comprehensive text block metadata including centering, heading, footnote, and poetry flags
Installation
# Using npm
npm install kokokor
# Using yarn
yarn add kokokor
# Using bun
bun add kokokorUsage
Basic Usage
import { formatTextBlocks, mapObservationsToTextLines, mapTextLinesToParagraphs } from 'kokokor';
// Example OCR result
const ocrResult = {
dpi: { x: 300, y: 300, width: 2480, height: 3508 },
observations: [
{ text: 'This is the first', bbox: { x: 100, y: 100, width: 200, height: 20 } },
{ text: 'line of text.', bbox: { x: 310, y: 100, width: 150, height: 20 } },
{ text: 'This is a new paragraph.', bbox: { x: 100, y: 150, width: 300, height: 20 } },
],
};
// Step 1: Convert observations to text lines
const textLines = mapObservationsToTextLines(ocrResult.observations, ocrResult.dpi, {});
// Step 2: Group text lines into paragraphs
const paragraphs = mapTextLinesToParagraphs(textLines);
// Step 3: Format as readable text
const reconstructedText = formatTextBlocks(paragraphs);
console.log(reconstructedText);
// Output:
// This is the first line of text.
// This is a new paragraph.Advanced Configuration
import { mapObservationsToTextLines, mapTextLinesToParagraphs } from 'kokokor';
const options = {
pixelTolerance: 5, // Tolerance for vertical alignment in lines
lineHeightFactor: 0.3, // Fixed line height factor (optional, otherwise computed adaptively)
// Centering detection options
centerToleranceRatio: 0.05, // Tolerance for center point alignment (5% of page width)
minMarginRatio: 0.2, // Minimum margin required for centering detection (20% of page width)
// Poetry detection options
poetryDetectionOptions: {
centerToleranceRatio: 0.05,
minMarginRatio: 0.1,
maxVerticalGapRatio: 2.0, // Max gap between poetry hemistichs
minWidthRatioForMerged: 0.6, // Minimum width for wide poetic lines
minWordCount: 2, // Minimum words for poetry consideration
pairWidthSimilarityRatio: 0.4, // Width similarity for poetry pairs
pairWordCountSimilarityRatio: 0.5, // Word count similarity for poetry pairs
wordDensityComparisonRatio: 0.95, // Density comparison for wide poetry lines
},
// Layout structure (optional)
horizontalLines: [], // Array of horizontal line bounding boxes for footnote detection
rectangles: [], // Array of rectangle bounding boxes for heading detection
// Debug logging (optional)
log: console.log,
};
// Process with advanced options
const textLines = mapObservationsToTextLines(observations, dpi, options);
const paragraphs = mapTextLinesToParagraphs(textLines, 2, 0.85); // verticalJumpFactor=2, widthTolerance=0.85Working with Surya OCR Results
kokokor can handle surya library output.
import { mapMatrixToBoundingBox } from 'kokokor';
// Convert Surya OCR format to kokokor observations
const suryaResult = {
text_lines: [
{
bbox: [100, 100, 400, 120], // [x1, y1, x2, y2] format
text: 'Text from Surya OCR',
},
],
};
// Convert Surya bounding boxes to kokokor format
const observations = suryaResult.text_lines.map((line) => ({
text: line.text,
bbox: mapMatrixToBoundingBox(line.bbox as [number, number, number, number]),
}));
// Now you can use these observations with kokokorWorking with Layout Elements
import { filterHorizontalLinesOutsideRectangles, calculateDPI } from 'kokokor';
// Calculate DPI from image and PDF dimensions
const dpi = calculateDPI(
{ width: 2480, height: 3508 }, // Image size
{ width: 595, height: 842 }, // PDF size in points
);
// Filter horizontal lines that aren't inside rectangles
const relevantLines = filterHorizontalLinesOutsideRectangles(
rectangles, // Array of rectangle bounding boxes
horizontalLines, // Array of horizontal line bounding boxes
5, // Pixel tolerance
);API Reference
Main Processing Functions
mapObservationsToTextLines(observations: Observation[], dpi: BoundingBox, options: MapObservationsToTextLinesOptions): TextBlock[]
Converts OCR observations into structured text lines with metadata.
Groups observations into lines based on vertical proximity, applies centering detection, identifies headings (text within rectangles), footnotes (text below horizontal lines), and poetic content.
- Parameters:
observations: Array of OCR text observationsdpi: Document DPI information including width and heightoptions: Configuration options for text line processing
- Returns: Array of text blocks with metadata (centering, headings, footnotes, poetry)
mapTextLinesToParagraphs(textLines: TextBlock[], verticalJumpFactor?: number, widthTolerance?: number): TextBlock[]
Groups text lines into coherent paragraphs, handling both prose and poetry.
Prose lines are grouped into paragraphs based on vertical spacing and line width patterns. Poetic lines are preserved individually to maintain their formatting.
- Parameters:
textLines: Array of text lines to group into paragraphsverticalJumpFactor: Factor for detecting paragraph breaks based on vertical spacing (default: 2)widthTolerance: Threshold for identifying "short" lines that indicate paragraph breaks (default: 0.85)
- Returns: Array of text blocks representing complete paragraphs
formatTextBlocks(textBlocks: TextBlock[], footerSymbol?: string): string
Formats an array of text blocks into a readable string with proper paragraph breaks.
- Parameters:
textBlocks: Array of text blocks to formatfooterSymbol: Optional symbol to insert before the first footnote
- Returns: Formatted text string with proper line breaks and spacing
Utility Functions
flipAndAlignObservations(observations: Observation[], imageWidth: number, dpiX: number, options?: object): Observation[]
Preprocesses observations by filtering noise, flipping coordinates for RTL text, and normalizing x-coordinates for proper alignment.
mapMatrixToBoundingBox(box: [number, number, number, number]): BoundingBox
Converts bounding box coordinates from array format to object format.
calculateDPI(imageSize: Size, pdfSize: Size): {x: number, y: number}
Calculates the DPI based on image size and original PDF size.
filterHorizontalLinesOutsideRectangles(rectangles: BoundingBox[], horizontalLines: BoundingBox[], tolerance?: number): BoundingBox[]
Filters out horizontal lines that are contained within any of the provided rectangles.
Types
TextBlock
type TextBlock = Observation & {
isCentered?: boolean; // If the text is centered on the page
isFootnote?: boolean; // If this text is a footnote
isHeading?: boolean; // If the text represents a heading
isPoetic?: boolean; // Is a line of poem (not merged into paragraphs)
};Observation
type Observation = {
bbox: BoundingBox; // Position and dimensions
text: string; // Text content
};BoundingBox
type BoundingBox = {
x: number; // X-coordinate
y: number; // Y-coordinate
width: number; // Width
height: number; // Height
};MapObservationsToTextLinesOptions
type MapObservationsToTextLinesOptions = {
pixelTolerance?: number; // Default: 5
lineHeightFactor?: number; // Optional fixed line height factor
centerToleranceRatio?: number; // Default: 0.05
minMarginRatio?: number; // Default: 0.2
poetryDetectionOptions?: PoetryDetectionOptions;
horizontalLines?: BoundingBox[]; // For footnote detection
rectangles?: BoundingBox[]; // For heading detection
log?: (message: string, ...args: any[]) => void; // Debug logging
};PoetryDetectionOptions
type PoetryDetectionOptions = {
centerToleranceRatio: number; // Default: 0.05
minMarginRatio: number; // Default: 0.1
maxVerticalGapRatio: number; // Default: 2.0
minWidthRatioForMerged: number; // Default: 0.6
minWordCount: number; // Default: 2
pairWidthSimilarityRatio: number; // Default: 0.4
pairWordCountSimilarityRatio: number; // Default: 0.5
wordDensityComparisonRatio: number; // Default: 0.95
};Algorithm Overview
Text Line Grouping
- Preprocessing: Filters noise, flips coordinates for RTL text, normalizes x-coordinates
- Adaptive Line Detection: Uses document spacing analysis to compute optimal line height factors
- Vertical Grouping: Groups observations into lines based on vertical proximity
- Horizontal Sorting: Sorts observations within each line by x-coordinate for proper reading order
- Metadata Assignment: Identifies centered text, headings, footnotes, and poetry
Poetry Detection
The library uses multiple heuristics to identify poetic content:
- Wide Poetic Lines: Centered text with low word density compared to prose
- Poetry Pairs (Hemistichs): Two lines with similar width and word count that are centered as a unit
- Centering Analysis: Uses configurable tolerances for center point alignment and margin requirements
- Word Density Comparison: Compares line density against document prose baseline
Paragraph Formation
- Poetry Preservation: Poetic lines are kept separate and not merged into paragraphs
- Vertical Gap Analysis: Uses vertical spacing patterns to identify paragraph breaks
- Line Width Analysis: Short lines often indicate paragraph endings
- Separate Processing: Body content and footnotes are processed independently
Testing
The project includes comprehensive integration tests for OCR paragraph reconstruction. You can control test behavior using environment variables for convenience during development.
Running Tests
# Run all tests with coverage
bun test
# Write/update test snapshots
bun run test:write
# Test only specific files
ONLY="1.jpg,2.jpg" bun test
# Combine snapshot writing with specific files
ONLY="example.jpg" bun run test:writeTest Environment Variables
WRITE_SNAPSHOTS=true- Updates expected test output files instead of comparing against themONLY="file1,file2"- Restricts testing to specific image files (comma-separated)
Examples
# Update snapshots for all tests
WRITE_SNAPSHOTS=true bun test
# Test and update snapshots for specific files only
WRITE_SNAPSHOTS=true ONLY="complex-document.jpg,simple-text.jpg" bun test
# Quick test of a single file during development
ONLY="debug-case.jpg" bun testContributing
Contributions are welcome! Please make sure your contributions adhere to the coding standards and are accompanied by relevant tests.
To get started:
- Fork the repository
- Install dependencies:
bun install(requires Bun) - Make your changes
- Run tests:
bun test - Submit a pull request
License
kokokor is released under the MIT License. See the LICENSE.MD file for more details.
Author
Ragaeeb Haq
Built with TypeScript and Bun. Uses ESM module format.
