Skip to main content

pyqv

Python library and CLI tool to read, decompress, and extract Altura QuickView databases.

CI Python License: MIT


Overview

pyqv provides a pure-Python parser and decompressor for Altura QuickView reference databases.

Features

  • Fixed 12-bit LZW Decompressor: Full pure-Python implementation of Altura's custom 12-bit adaptive LZW codec (decompress_lzw).
  • Complete File Format Parser: Reads 32-byte headers, 22-byte block topic maps, keyword indices, cross-reference tables, and embedded QuickDraw PICT images.
  • CLI Tool: pyqv command-line utility for inspecting, extracting, and searching .qv databases.
  • Damage-tolerant: malformed input raises a QVError instead of hanging, crashing, or returning silent garbage.

No third-party dependencies; Python 3.8 or newer.


Installation

Not yet published to PyPI. Install from source:

git clone https://github.com/codybrom/pyqv.git
cd pyqv
pip install -e .

To run the test suite:

pip install -e ".[test]"
pytest

Python API Usage

1. Opening a Database and Reading Topics

import pyqv

# Open a .qv database
db = pyqv.read("Power PC.qv")

print(f"Total topics: {len(db.topics)}")
print(f"Total keywords: {len(db.keywords)}")

# Read and decompress a specific compressed block (kind == 160)
for topic in db.topics:
    if topic.is_compressed:
        prose_bytes = db.read_topic_data(topic)
        text = prose_bytes.decode("mac_roman", errors="replace")
        print(f"Topic Group {topic.group} #{topic.index}:")
        print(text[:200])
        break

2. Standalone LZW Decompression

from pyqv import decompress_lzw

# Pass raw compressed bytes (kind == 160 block)
compressed_data = b"..."  # raw compressed bytes from .qv block
decompressed_text = decompress_lzw(compressed_data)

print(decompressed_text.decode("mac_roman"))

3. Extracting Embedded QuickDraw PICT Figures

Figures are located by their PICT version 2 signature rather than by group tag, so they are found wherever a database stores them. Note that topic.index is not unique across the topic table, so enumerate when naming files:

import pyqv

db = pyqv.read("Macintosh Toolbox.qv")
pictures = db.get_pictures()

for position, (topic, pict_bytes) in enumerate(pictures):
    with open(f"figure_{position:04d}.pict", "wb") as f:
        f.write(pict_bytes)

4. Handling Damaged Files

These databases are 30 years old, and blocks do not always survive intact. Every failure pyqv detects derives from QVError:

Exception Meaning
QVError Base class; catch this to handle any pyqv failure
InvalidDatabaseError Bad magic, header invariants violated, or a structure running past end of file
CorruptBlockError A compressed block drives the LZW dictionary into a cycle, which no valid encoder produces
import pyqv

db = pyqv.read("Power PC.qv")

for topic in db.topics:
    try:
        data = db.read_topic_data(topic)
    except pyqv.QVError as exc:
        print(f"skipping {topic}: {exc}")
        continue
    ...

Decoding to nothing is not an error: an empty block, one too short to hold a single code, and one whose first code names a slot no literal hashed into all return b"". That last case is common — about half the compressed blocks in a real database decode to nothing — so treating it as damage badly overstates how much of a file is broken. The CLI reports the two separately, and a genuinely damaged block produces a warning on stderr while the run continues.


Command Line Interface (CLI)

pyqv comes with a command-line tool for quick inspection, extraction, and searching.

Inspect a Database

pyqv inspect "Power PC.qv"

Output:

File: Power PC.qv
  Size: 329,793 bytes
  Topics: 1,013
  Keywords: 199
  Topic Table: 172,145..194,431
  Trailer: 195,023..329,793
  Compressed Blocks (kind=160): 89, 114,961 bytes
  Decompressed Text: 84,792 bytes
  Sample Text: Mixed Mode Manager | CallOSTrapUniversalProc | DisposeRoutineDescriptor ...
  QuickDraw Pictures (PICT): 3, 34,914 bytes

Extract Text and Figures

pyqv extract "Power PC.qv" --output ./extracted_powerpc

Text blocks are written to <output>/text and figures to <output>/pictures. Filenames carry the record's position in the topic table as well as its index, because index alone is not unique and would let one block overwrite another:

extracted_powerpc/text/group_32_00007_idx_00003.txt
extracted_powerpc/pictures/pic_0000_idx_0012.pict

Search Keywords and Decompressed Prose

pyqv search "Power PC.qv" "ExceptionHandler"

Technical Details

LZW Codec Specification

QuickView compresses topic prose using a custom variant of Lempel-Ziv-Welch (LZW):

  • Fixed 12-bit code width (0..4095).
  • 2 codes packed into 3 bytes, MSB first (code0 = (b0 << 4) | (b1 >> 4), code1 = ((b1 & 0x0f) << 8) | b2).
  • 4,096-node array initialized with 256 root nodes (0..255). Hash table collision step is 0x65 ((curr + 0x65) & 0xfff).

A code is a dictionary slot index, and entries are placed by hash, not sequentially. An entry for (parent, char) goes wherever ((parent + char | 0x800)² >> 6) & 0xfff points, then follows the chain and probes by 0x65. Two consequences trip up anyone porting this from a textbook LZW implementation:

  • Slots 0..255 are not the literals. The 256 roots are inserted by the same hash as everything else, so a first code naming an empty slot is ordinary — that block simply decodes to nothing. Roughly half the compressed blocks in a real database do exactly that.
  • A new entry has no reason to land on the code that referenced it. In textbook LZW, a code used before it is defined (the KwKwK case) resolves to the next sequential index. Here the slot is wherever the hash put it. Requiring the two to match rejects most real blocks.

The 256 literals plus the 3,840 entries a stream may add fill the table exactly, which is why allocation never runs out of slots. Only one condition is genuinely unrecoverable: a corrupt block can leave a code's slot empty while a later insert points a node's parent at it, closing a cycle in what should be a strictly-decreasing chain. decompress_lzw bounds that walk and raises CorruptBlockError rather than following the loop forever.

Testing

The databases are copyrighted Apple documentation, so no .qv fixture ships in this repository. tests/lzw_reference.py implements a reference compressor against the same dictionary layout, letting the codec be verified round-trip on generated corpora (including the KwKwK case and inputs that exhaust the dictionary), and tests/qv_builder.py assembles synthetic containers in memory.

Know what this can and cannot show. The reference encoder shares the decoder's model of the format, so a round trip proves the two agree — not that either is right. An assumption wrong in both passes every test here. The suite therefore also pins the specific places where a textbook-LZW reading of this format is wrong (TestKwKwKPlacement, TestDecodesToNothing), because those are the mistakes a round-trip test cannot catch on its own.

Anything claiming the decoder is correct, rather than self-consistent, has to be checked against output from the real application. That verification is done out-of-tree against prose captured from QuickView 2.0c's memory, since neither the databases nor the captures are redistributable.


License

MIT License. See LICENSE for details.

Metadata

Release files for pyqv 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pyqv 0.1.0
File Size Uploaded
pyqv-0.1.0.tar.gz 25.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pyqv 0.1.0
File Interpreter ABI Platform
pyqv-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 41.1 kB

Release files / pyqv-0.1.0.tar.gz

Download URL pyqv-0.1.0.tar.gz
Size 25.7 kB
Tags Source
SHA-256 checksum
How to use checksums
cb9f03e1997ffe11481ab56c70568fccb5e7204194efe26acdaa55bce2523270
BLAKE2b-256 checksum
How to use checksums
90b9e16d9eb26683e30be0ae3c07b94825513602dba364b879fd75acd22a0464
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 16, 2026.

Transparency log

Release files / pyqv-0.1.0-py3-none-any.whl

Download URL pyqv-0.1.0-py3-none-any.whl
Size 15.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
874ec0569399294337d32faa8d6dc8543e90a441709d59f6c704f0c88582cf2c
BLAKE2b-256 checksum
How to use checksums
caef8272245817b5bc5c722609bffc6149fa01bf38e4755022e35cab83c6a237
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 16, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page