My Projects

🔍 Check out my latest project

Compare Word documents online with character-level precision

Visit DocumentDelta.com →

Friday, April 17, 2026

Why DOCX Comparison Is Harder Than You Think

Most people think comparing two Word documents is like comparing two text files. Find the differences, highlight them, done.

It's not. Not even close.

I've spent years working on this problem, and I built DocumentDelta to solve it. Here's what makes DOCX comparison so brutally hard — and what I learned building a comparison engine from scratch.

A DOCX File Is Not Text

A .docx file is a ZIP archive containing XML. A single paragraph with one bold word might look like this under the hood:

<w:p>
  <w:r><w:t>This is </w:t></w:r>
  <w:r><w:rPr><w:b/></w:rPr><w:t>important</w:t></w:r>
  <w:r><w:t> text.</w:t></w:r>
</w:p>

Three separate "runs" for one sentence. Now imagine diffing two documents where the same text is split into runs differently. Text-level comparison breaks immediately.

The Five Hardest Problems

1. Field Codes Are Invisible Landmines

A cross-reference like "see Section 3.1" isn't just text. In the XML, it's a multi-element structure: a fldChar begin marker, an instrText containing something like REF _Ref488222269 \h, a separator, the visible result text, and an end marker.

If your diff algorithm matches individual runs, it shatters field groups. The result? "Error! Reference source not found" scattered throughout the document — even in paragraphs that didn't change. I've seen tools destroy 75% of cross-references in unchanged content because they treated field codes as regular text.

2. Everything Shifts When You Insert a Section

Insert a new clause in the middle of a contract. Now every subsequent section number changes. Bookmark IDs get renumbered. Page references update. TOC entry prefixes shift from "12.1" to "13.1".

A naive comparison algorithm sees every subsequent paragraph as "changed" because the numbering is different — even though the actual content is identical. You need to strip dynamic content during matching, then diff the actual changes separately.

3. You Can't Delete a Table Column the Normal Way

In Open XML, you can wrap text in w:del to mark it as deleted. Simple enough. But try wrapping a table cell (w:tc) in w:del — Word rejects the file.

Column deletions require specialized revision elements: w:tblGridChange on the grid definition, w:cellDel markers in the cell properties, and content marked as deleted inside the cell. Most comparison tools either corrupt the file or duplicate rows when columns change.

4. Content Controls Can't Be Children of Revisions

Content controls (w:sdt) — used for structured fields, TOC wrappers, and form elements — cannot be placed inside w:ins or w:del tags. The OOXML spec forbids it.

So how do you track changes inside a content control? You have to preserve the SDT wrapper intact and mark changes inside w:sdtContent. Get this wrong with picture content controls and Word crashes — they use w14: namespace attributes that must be preserved exactly.

5. The Interleaved Phrase Problem

"Balancing Amount" changes to "Earn-out Consideration". A character-level diff algorithm sees the space character as a common substring and produces:

BalancingEarn-out AmountConsideration

This is technically correct but completely unreadable. The fix is non-obvious: tokenize at word boundaries with trailing whitespace attached, so the diff produces clean output:

Balancing Amount Earn-out Consideration

This kind of cleanup is the difference between a comparison tool that's technically right and one that's actually useful.

What Clean Output Requires

Getting readable tracked changes isn't just about finding differences. The output needs multiple cleanup passes: merging fragmented whitespace-adjacent edits, absorbing trailing whitespace into strikethrough ranges, collapsing tiny unchanged segments sandwiched between deletions and insertions, and generating proper revision markup elements with exactly the right XML wrapper structure.

I've had bugs where Word showed zero tracked changes because of a single missing XML wrapper element. Valid-looking XML that Word "repairs" by silently deleting bookmarks. Documents that open fine but have 18 more bookmark starts than ends.

The Numbers

DocumentDelta compares a ~230KB legal Share Purchase Agreement — a real enterprise contract — in about 473 milliseconds. The output is a complete, valid DOCX with native Word tracked changes that you can open and accept/reject normally.

It handles tables, text boxes, headers, footers, footnotes, endnotes, comments, hyperlinks, images, bookmarks, field codes, content controls, and pre-existing tracked changes.

279 tests. All passing.

Try It

If you work with DOCX files at scale — in legal, compliance, publishing, or enterprise document automation — give it a try:

www.documentdelta.com/demo

There's also a REST API for integration into your workflows. Contact me for details.


Find me on LinkedIn or check out DocumentDelta.com.

Friday, December 1, 2017

HTML to PDF without third party solutions



There are times when we need to convert a html page to pdf. There are a few options we can use to do this
  1. Use third Party solutions
  2. Creating a html render engine
  3. Using an existing html render engine like Firefox, chromium etc
Small projects or indie developers might need some simple and free way to provide the functionality to its customers.
Process
The problem statement is: We have html code which is rendered in a web browser and we need to convert that rendered page into pdf. The thinking process and workflow looks like this:
  1. Rendering the html code
As we only have the html code, we require a render engine to convert that code into readable page. Maybe we can use internet explorer’s engine which is used by the web browser control in a WinForms application. We can use different engine versions of internet explorer by placing specific html tags in head part.
  1. Taking a screen shot
We can take a screenshot of the rendered page and create an image of it. We can use the print screen functionality for this step. But print screen only takes what screen is showing and thus we would need to figure out how to take screenshot of the page even if its larger than the screen size.
  1. Putting the screen shot in a PDF
We can use itextsharp or similar libraries to create a pdf and put image file in it.
  1. All this in background
We need all this process in background to be executed from code without any human intervention
This way we can have pdf file of any rendered page from any html.
So here’s the entire code for the class which can do this process:
public static class Converter
   {
       public static Byte[] GetPdfFromHtml(string htmlfilePath)
       {
           if (!System.IO.File.Exists(htmlfilePath))
               return null;

           string imagefileName = System.IO.Path.GetTempFileName();
           string fileName = htmlfilePath;

           Thread pdfThread = new Thread(() => GenerateImage(fileName, imagefileName));
           pdfThread.SetApartmentState(ApartmentState.STA);
           pdfThread.Start();

           pdfThread.Join();

           var baseimage = new Bitmap(imagefileName);
           iTextSharp.text.Rectangle pageSize = new iTextSharp.text.Rectangle(baseimage.Width, baseimage.Height);
           baseimage.Dispose();

           string pdffileName = System.IO.Path.GetTempFileName();

           iTextSharp.text.Document document = new iTextSharp.text.Document(pageSize, 0, 0, 0, 0);

           using (var stream = new FileStream(pdffileName, FileMode.Create, FileAccess.Write, FileShare.None))
           {
               iTextSharp.text.pdf.PdfWriter.GetInstance(document, stream);
               document.Open();
               using (var imageStream = new FileStream(imagefileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
               {
                   var image = iTextSharp.text.Image.GetInstance(imageStream);
                   document.Add(image);
               }

               document.Close();
           }

           Byte[] bytes = System.IO.File.ReadAllBytes(pdffileName);
           System.IO.File.Delete(imagefileName);
           System.IO.File.Delete(pdffileName);
           return bytes;
       }

       private static Bitmap ResizeImage(System.Drawing.Image image, int width, int height)
       {
           var destRect = new System.Drawing.Rectangle(0, 0, width, height);
           var destImage = new Bitmap(width, height);

           destImage.SetResolution(image.HorizontalResolution, image.VerticalResolution);

           using (var graphics = Graphics.FromImage(destImage))
           {
               graphics.CompositingMode = CompositingMode.SourceCopy;
               graphics.CompositingQuality = CompositingQuality.HighQuality;
               graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
               graphics.SmoothingMode = SmoothingMode.HighQuality;
               graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;

               using (var wrapMode = new System.Drawing.Imaging.ImageAttributes())
               {
                   wrapMode.SetWrapMode(WrapMode.TileFlipXY);
                   graphics.DrawImage(image, destRect, 0, 0, image.Width, image.Height, GraphicsUnit.Pixel, wrapMode);
               }
           }

           return destImage;
       }

       private static void GenerateImage(string fileName, string imagefileName)
       {
           WebBrowser wb = new WebBrowser(); //WebBrowser control creation to render the page
           wb.ScrollBarsEnabled = false;
           wb.ScriptErrorsSuppressed = true;
           wb.Url = new Uri(fileName);

           while (wb.ReadyState != WebBrowserReadyState.Complete) //Wait to finish rendering
           {
               Application.DoEvents();
           }

           wb.Width = wb.Document.Body.ScrollRectangle.Width;
           wb.Height = wb.Document.Body.ScrollRectangle.Height;

           Bitmap screentshot = new Bitmap(wb.Width, wb.Height);
           GetImage(wb.ActiveXInstance, screentshot, Color.White); //Getting screen shot of the rendered page

           screentshot.Save(imagefileName);

           wb.Dispose();
       }

       [ComImport]
       [Guid("0000010D-0000-0000-C000-000000000046")]
       [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
       interface IViewObject
       {
           void Draw([MarshalAs(UnmanagedType.U4)] uint dwAspect, int lindex, IntPtr pvAspect, [In] IntPtr ptd, IntPtr hdcTargetDev, IntPtr hdcDraw, [MarshalAs(UnmanagedType.Struct)] ref RECT lprcBounds, [In] IntPtr lprcWBounds, IntPtr pfnContinue, [MarshalAs(UnmanagedType.U4)] uint dwContinue);
       }

       [StructLayout(LayoutKind.Sequential, Pack = 4)]
       struct RECT
       {
           public int Left;
           public int Top;
           public int Right;
           public int Bottom;
       }

       private static void GetImage(object obj, System.Drawing.Image destination, Color backgroundColor)
       {
           using (Graphics graphics = Graphics.FromImage(destination))
           {
               IntPtr deviceContextHandle = IntPtr.Zero;
               RECT rectangle = new RECT();

               rectangle.Right = destination.Width;
               rectangle.Bottom = destination.Height;

               graphics.Clear(backgroundColor);

               try
               {
                   deviceContextHandle = graphics.GetHdc();

                   IViewObject viewObject = obj as IViewObject;
                   viewObject.Draw(1, -1, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, deviceContextHandle, ref rectangle, IntPtr.Zero, IntPtr.Zero, 0);
               }
               finally
               {
                   if (deviceContextHandle != IntPtr.Zero)
                   {
                       graphics.ReleaseHdc(deviceContextHandle);
                   }
               }
           }
       }
   }