<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Print-Scan Channel on Boris Dupoux</title><link>https://borisdupoux.com/tags/print-scan-channel/</link><description>Recent content in Print-Scan Channel on Boris Dupoux</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Tue, 01 Sep 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://borisdupoux.com/tags/print-scan-channel/index.xml" rel="self" type="application/rss+xml"/><item><title>Codex — High-Density Physical Archiving of Media on Paper</title><link>https://borisdupoux.com/research/codex-paper-archive/</link><pubDate>Tue, 01 Sep 2026 00:00:00 +0000</pubDate><guid>https://borisdupoux.com/research/codex-paper-archive/</guid><description>&lt;h2 id="context"&gt;&lt;a href="#context" class="header-anchor"&gt;&lt;/a&gt;Context
&lt;/h2&gt;&lt;p&gt;Consumer digital media decay on the scale of years. Unpowered SSDs lose charge, optical discs delaminate, formats go out of use and accounts get closed. Paper can last for centuries, needs no electricity and can be checked by eye. The question is how much information an ordinary A4 sheet can hold if it must still be read back reliably with a standard flatbed scanner and no special hardware.&lt;/p&gt;
&lt;p&gt;The problem is hard because the print-and-scan channel is noisy in several ways at once. Toner spreads and clumps, density varies across the page, and scanners add rotation, anisotropic scaling, uneven paper feed, blur, vignetting and gamma. Paper also stretches non-linearly: at the target density, 0.2 % of stretch shifts the grid by about three cells. Common 2D codes such as QR and DataMatrix are designed for robustness rather than density. A long-term archive also has to remain decodable when today&amp;rsquo;s software no longer exists.&lt;/p&gt;
&lt;h2 id="approach"&gt;&lt;a href="#approach" class="header-anchor"&gt;&lt;/a&gt;Approach
&lt;/h2&gt;&lt;pre class="mermaid" style="visibility:hidden"&gt;flowchart TB
 A["Source media: photo, video, voice, text"] --&gt; B["Format-conservative reduction&lt;br/&gt;JPEG baseline · MJPEG/PCM AVI · PCM WAV · UTF-8"]
 B --&gt; C["Archive: tar + zlib + SHA-256"]
 C --&gt; D["Inter-page erasure code&lt;br/&gt;Cauchy MDS over GF(256): any K of N pages"]
 D --&gt; E["Per-block Reed–Solomon + CRC-32&lt;br/&gt;page-wide interleaving, triplicated header"]
 E --&gt; F["Multi-level cell rendering&lt;br/&gt;1-bit A4 PDF at 1200 dpi"]
 F --&gt; G["Print → scan&lt;br/&gt;(simulated channel)"]
 G --&gt; H["Fiducials → homography → local alignment markers"]
 H --&gt; I["Grey-level calibration from the page itself"]
 I --&gt; J["RS/CRC decode → erasure decode → SHA-256 check"]
 J --&gt; K["Byte-exact files, or partial recovery"]
 F -.-&gt; L["Printed appendix: format spec + decoder source in OCR-B with per-line CRC-16"]
 L -.-&gt; J&lt;/pre&gt;&lt;p&gt;&lt;img alt="A full encoded A4 data page from the photo archive, next to a printer-resolution close-up of its top-left corner showing the corner fiducial, the start of the header band and the first grey-level cells with an alignment marker" class="gallery-image" data-flex-basis="436px" data-flex-grow="181" height="880" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://borisdupoux.com/research/codex-paper-archive/codex-page-and-macro.png" srcset="https://borisdupoux.com/research/codex-paper-archive/codex-page-and-macro_hu_7e58d21b3fd6152e.png 800w, https://borisdupoux.com/research/codex-paper-archive/codex-page-and-macro.png 1600w" width="1600"&gt;
&lt;em&gt;Left: page 1 of 10 of the photo archive, as sent to the printer (A4, 1200 dpi, a grid of 1,145 × 1,576 cells), downscaled. Right: the boxed top-left corner at printer resolution (about 10 × 9 mm, each printer dot drawn as 2 × 2 pixels): the solid corner fiducial, the start of the header band, and the first data cells. Each cell is 8 × 8 dots (0.169 mm) and its number of inked dots encodes one of 4 levels. The square at the corner of the grid is an alignment marker.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="A page of the printed appendix: the start of the decoder file cdx_read.py, set in OCR-B, each line preceded by its number and a four-digit CRC-16" class="gallery-image" data-flex-basis="339px" data-flex-grow="141" height="778" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://borisdupoux.com/research/codex-paper-archive/codex-appendix-decoder-page.png" srcset="https://borisdupoux.com/research/codex-paper-archive/codex-appendix-decoder-page_hu_f0b09cea3a0e024f.png 800w, https://borisdupoux.com/research/codex-paper-archive/codex-appendix-decoder-page.png 1100w" width="1100"&gt;
&lt;em&gt;A page of the printed appendix (page 11 of 30): the start of &lt;code&gt;cdx_read.py&lt;/code&gt; (360 lines), set in OCR-B. Each line carries its number and its CRC-16, so a line misread by OCR can be found and checked by hand.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="methods"&gt;&lt;a href="#methods" class="header-anchor"&gt;&lt;/a&gt;Methods
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Multi-level cells instead of binary modules.&lt;/strong&gt; Each cell carries 2 bits. Cells are drawn as clustered dots at the printer&amp;rsquo;s native resolution, so the driver does no halftoning and produces no moiré.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Geometry recovery in stages.&lt;/strong&gt; Four corner fiducials, one of them hollow to fix orientation, give a global homography. Alignment markers spread across the page then absorb local, non-linear paper stretch.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Self-calibrating reader.&lt;/strong&gt; The decoder estimates the printed grey levels and spatial shading from calibration structures on each page, so it does not depend on the printer&amp;rsquo;s dot settings.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Two-level error control, a product code.&lt;/strong&gt; Reed–Solomon and CRC-32 inside each page correct random and burst errors such as stains and scratches. A Cauchy MDS erasure code across pages, decoded block by block, lets any K of N pages rebuild an archive.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Graceful failure.&lt;/strong&gt; If the SHA-256 check fails, the decoder still extracts every complete file that comes before the damaged region.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Self-describing archive.&lt;/strong&gt; A printed appendix contains the format specification, a reading procedure and the full decoder source in OCR-B, with a line number and CRC-16 on each line. The decoder needs only NumPy, Pillow and reedsolo. Text uses DEFLATE (zlib), which can be reimplemented in a few hundred lines, rather than the stronger LZMA.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Earlier prototype (&lt;code&gt;codex_proto/&lt;/code&gt;).&lt;/strong&gt; This version explored several physical media: paper, laser-marked stainless steel, intaglio plates and microfilm frames. It used 2- to 16-level cells, fountain-style parity pages, MLP-based grey-level readers, a printed optics calculator for reading microfilm, and lossy media reduction (parametric voice coding, wavelet-residual photo coding).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="results"&gt;&lt;a href="#results" class="header-anchor"&gt;&lt;/a&gt;Results
&lt;/h2&gt;&lt;p&gt;All figures below come from &lt;strong&gt;simulation only&lt;/strong&gt;. The software models laser defects (spot blur, toner grain, dot gain, banding, non-uniform density, textured paper), scanner defects (±1.2° rotation, ±0.3 % x/y scale, feed wobble, blur, vignetting, noise, gamma 2.2), coffee stains, scratches, and &lt;strong&gt;one deliberately lost page per archive&lt;/strong&gt;. No physical print-and-scan test has been run yet.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Net payload:&lt;/strong&gt; 383,469 bytes per A4 page, about 59 bits/mm².&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;End-to-end:&lt;/strong&gt; 46 of 50 pages were scanned and all &lt;strong&gt;4 of 4 archives were restored byte-identical&lt;/strong&gt; (photo, video, voice, text). No Reed–Solomon codeword failed, and no inter-page block was lost.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Error rates:&lt;/strong&gt; clean pages needed about 0.007–0.008 % of bytes corrected. Pages with stains and/or scratches needed at most 0.90 %.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Archive sizes:&lt;/strong&gt; photo 2.86 MB → 10 pages (8 data + 2 parity); video (800×600, 24 s, MJPEG + 16 kHz PCM) 8.30 MB → 25 pages; voice 0.91 MB → 5 pages; text 5.58 MB → 10 pages.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Text:&lt;/strong&gt; the public-domain &lt;em&gt;Mémoires d&amp;rsquo;outre-tombe&lt;/em&gt; (5.43 M characters) would take 1,357 typeset A4 pages. Encoded, it takes 6 data pages, a ratio of about 226. zlib compresses this prose by 2.62 and LZMA by 3.58.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Partial recovery:&lt;/strong&gt; an archive truncated to a quarter of its length still returned 1.42 MB of the 5.58 MB of text.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Decoder robustness:&lt;/strong&gt; the decoder also worked on scans rotated 90° and 180°, on 600 dpi scans and on JPEG (q92) scans.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Decoder size:&lt;/strong&gt; 731 lines of Python for the printed decoder modules.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img alt="The same small region of a data page, left as encoded and right after the simulated print-and-scan channel, crossing the edge of a simulated coffee stain" class="gallery-image" data-flex-basis="616px" data-flex-grow="256" height="600" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://borisdupoux.com/research/codex-paper-archive/codex-encoded-vs-scan.png" srcset="https://borisdupoux.com/research/codex-paper-archive/codex-encoded-vs-scan_hu_25056d9200d46d29.png 800w, https://borisdupoux.com/research/codex-paper-archive/codex-encoded-vs-scan.png 1540w" width="1540"&gt;
&lt;em&gt;The same region of the photo page (380 × 300 dots, about 8 × 6.4 mm), enlarged. Left: as encoded. Right: after the &lt;strong&gt;simulated&lt;/strong&gt; print-and-scan channel, resampled onto the page grid through the fiducial homography. Toner spread, blur and noise soften the cells, and the edge of a simulated coffee stain darkens the left part. The reader calibrates the grey levels locally, so it still separates the four levels. In the simulation run reported above, stained or scratched pages needed at most 0.90 % of bytes corrected.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="Three frames of an underwater video, original on top and restored after encoding, simulated print-and-scan and decoding on the bottom" class="gallery-image" data-flex-basis="480px" data-flex-grow="200" height="792" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://borisdupoux.com/research/codex-paper-archive/codex-video-frames.jpg" srcset="https://borisdupoux.com/research/codex-paper-archive/codex-video-frames_hu_87a931ee2368d927.jpg 800w, https://borisdupoux.com/research/codex-paper-archive/codex-video-frames.jpg 1584w" width="1584"&gt;
&lt;em&gt;The video archive: an underwater clip (school of fish over a reef) used as test footage. Top: the original 3840 × 2880 HEVC frames at 3.2 s, 12.8 s and 16.0 s, downscaled. Bottom: the same instants decoded from the &lt;strong&gt;simulated&lt;/strong&gt; scans of the 25 pages (one deliberately lost). The paper channel itself loses nothing: the restored AVI is byte-identical to the archived one. What differs from the original is the reduction made before printing (800 × 600 MJPEG, 12.5 frames/s, 16 kHz mono sound) so that 24 s of video fit in 8.30 MB.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="Close-up of the same reef area, original video on the left and restored video on the right, showing JPEG blocking and loss of fine detail in the restored frame" class="gallery-image" data-flex-basis="648px" data-flex-grow="270" height="585" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://borisdupoux.com/research/codex-paper-archive/codex-video-detail.jpg" srcset="https://borisdupoux.com/research/codex-paper-archive/codex-video-detail_hu_ef2152090c0bf4cc.jpg 800w, https://borisdupoux.com/research/codex-paper-archive/codex-video-detail.jpg 1580w" width="1580"&gt;
&lt;em&gt;Close-up of the 12.8 s frame. Left: a 1152 × 864-pixel area of the original, downscaled. Right: the matching 240 × 180-pixel area of the restored frame, enlarged with no smoothing. The fish and the reef stay recognisable. Fine texture and colour detail are lost, and MJPEG block artefacts appear. This is the price of fitting the clip into 25 pages.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="limitations--next-steps"&gt;&lt;a href="#limitations--next-steps" class="header-anchor"&gt;&lt;/a&gt;Limitations &amp;amp; next steps
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No physical test yet.&lt;/strong&gt; The cell dot patterns were tuned in simulation. The next step is a real 1200 dpi print with a 1200 dpi scan to set them and measure the actual error margin.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Small test set.&lt;/strong&gt; The simulation covers one run over four archives. A sweep across defect severity, scan resolution and paper stock is still needed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Unmeasured physical properties.&lt;/strong&gt; Long-term ageing of paper and toner, and how practical it is to OCR the decoder back from paper, have not been measured.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Font dependency.&lt;/strong&gt; The appendix body text relies on a system font.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="stack"&gt;&lt;a href="#stack" class="header-anchor"&gt;&lt;/a&gt;Stack
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;Python&lt;/code&gt; · &lt;code&gt;NumPy&lt;/code&gt; · &lt;code&gt;SciPy&lt;/code&gt; · &lt;code&gt;Pillow&lt;/code&gt; · &lt;code&gt;reedsolo&lt;/code&gt; · &lt;code&gt;ReportLab&lt;/code&gt; · &lt;code&gt;soundfile&lt;/code&gt; · &lt;code&gt;ffmpeg&lt;/code&gt; · &lt;code&gt;Tesseract (OCR-B)&lt;/code&gt;&lt;/p&gt;</description></item></channel></rss>