Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/architecture/visual-previews.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ Every preview records the complete recipe that can affect its bytes:

The canonical recipe bytes produce a stable fingerprint. Changing any of these
choices creates a new immutable generation instead of rewriting an earlier
result.
result. The processor fingerprint is a maintained descriptor independent of
the Go runtime. Changing a byte-producing implementation or policy requires a
deliberate descriptor revision.

## Durable outcomes

Expand Down
5 changes: 5 additions & 0 deletions docs/internal/storage-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ These layers must not be collapsed. Node reachability is product policy; blob
membership is docbank's physical authority boundary; offsets, reader caches,
and repacking are storage mechanics.

Visual-preview generations use the canonical recipe fingerprint. The local
processor descriptor names byte-producing choices without treating the ambient
Go runtime version as identity; a descriptor revision is the deliberate
re-render signal.

Stable node IDs are document identity. Paths are derived from parent/name rows
and can change or be reused. Blob hashes are content identity. Two nodes may
share a blob without sharing document identity.
Expand Down
19 changes: 9 additions & 10 deletions internal/processing/visual_preview.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import (
"image/png"
"io"
"mime"
"runtime"

xdraw "golang.org/x/image/draw"
"golang.org/x/image/webp"
Expand All @@ -37,18 +36,18 @@ const (
visualPreviewWebPAnimation = 1 << 1
visualPreviewWebPEXIF = 1 << 3
visualPreviewWebPICCProfile = 1 << 5
// Bump the descriptor revision when any byte-producing choice changes.
visualPreviewProcessorDescriptor = "docbank-visual-preview:jpeg+png+gif-stdlib+webp+embedded-camera-raw+x-image-draw-v0.44.0:max-edge=4096:quality=90:alpha=white:v7"
)

var visualPreviewRecipe = document.VisualPreviewRecipeV1{
ContractVersion: document.VisualPreviewContractV1,
MaxEdgePixels: visualPreviewMaxEdgePixels,
OutputMediaType: "image/jpeg",
OrientationPolicy: "apply",
ColorPolicy: "srgb",
FramePolicy: "primary",
ProcessorFingerprint: fingerprintVisualPreviewProcessor(
"docbank-visual-preview:jpeg+png+gif-stdlib-" + runtime.Version() +
"+webp+embedded-camera-raw+x-image-draw-v0.44.0:max-edge=4096:quality=90:alpha=white:v6"),
ContractVersion: document.VisualPreviewContractV1,
MaxEdgePixels: visualPreviewMaxEdgePixels,
OutputMediaType: "image/jpeg",
OrientationPolicy: "apply",
ColorPolicy: "srgb",
FramePolicy: "primary",
ProcessorFingerprint: fingerprintVisualPreviewProcessor(visualPreviewProcessorDescriptor),
}

// VisualPreviewTarget identifies one exact immutable source to process.
Expand Down
69 changes: 69 additions & 0 deletions internal/processing/visual_preview_fingerprint_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package processing

import (
"bytes"
"crypto/sha256"
"encoding/hex"
"fmt"
"image/color"
"runtime/debug"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"go.kenn.io/docbank/document"
"go.kenn.io/docbank/document/media/mediatest"
)

const (
pinnedVisualPreviewProcessorFingerprint = "6391e667d07b0aab1622d2b4167ffe66496fe7d336beb603401d96cd8a11a641"
pinnedVisualPreviewRecipeFingerprint = "03f89b744cc013004c89b1babe6d779ee043652519c586453bcde761bd7a4b04"
)

func TestVisualPreviewProcessorFingerprintIsPinned(t *testing.T) {
assert.Equal(t, pinnedVisualPreviewProcessorFingerprint,
CurrentVisualPreviewRecipe().ProcessorFingerprint)
}

func TestVisualPreviewRecipeFingerprintIsPinned(t *testing.T) {
_, fingerprint, err := document.MarshalVisualPreviewRecipeV1(CurrentVisualPreviewRecipe())
require.NoError(t, err)
assert.Equal(t, pinnedVisualPreviewRecipeFingerprint, fingerprint)
}

func TestProducedVisualPreviewCarriesPinnedRecipe(t *testing.T) {
source := mediatest.JPEG(3, 2, color.White)
digest := sha256.Sum256(source)

product, err := ProduceVisualPreview(t.Context(), bytes.NewReader(source), VisualPreviewTarget{
SourceSHA256: hex.EncodeToString(digest[:]),
Size: int64(len(source)),
MediaType: "image/jpeg",
})
require.NoError(t, err)
assert.Equal(t, document.VisualPreviewReady, product.Preview.State)
assert.Equal(t, CurrentVisualPreviewRecipe(), product.Preview.Recipe)
assert.Equal(t, pinnedVisualPreviewProcessorFingerprint,
product.Preview.Recipe.ProcessorFingerprint)
_, fingerprint, err := document.MarshalVisualPreviewRecipeV1(product.Preview.Recipe)
require.NoError(t, err)
assert.Equal(t, pinnedVisualPreviewRecipeFingerprint, fingerprint)
}

func TestVisualPreviewDescriptorTracksLinkedDependenciesAndPolicy(t *testing.T) {
info, ok := debug.ReadBuildInfo()
require.True(t, ok)

var xImageVersion string
for _, dependency := range info.Deps {
if dependency.Path == "golang.org/x/image" {
xImageVersion = dependency.Version
break
}
}
assert.Equal(t, "v0.44.0", xImageVersion)
descriptor := ":" + visualPreviewProcessorDescriptor + ":"
assert.Contains(t, descriptor, ":"+fmt.Sprintf("max-edge=%d", visualPreviewMaxEdgePixels)+":")
assert.Contains(t, descriptor, ":"+fmt.Sprintf("quality=%d", visualPreviewJPEGQuality)+":")
}
4 changes: 4 additions & 0 deletions vault_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,10 @@ func TestVaultEnsureVisualPreviewProducesBoundedJPEG(t *testing.T) {
assert.Equal(t, 4096, preview.Output.Width)
assert.Equal(t, 2, preview.Output.Height)
assert.Equal(t, receipt.Version.ID, preview.Version.ID)
assert.Equal(t, "6391e667d07b0aab1622d2b4167ffe66496fe7d336beb603401d96cd8a11a641",
preview.Recipe.ProcessorFingerprint)
assert.Equal(t, "03f89b744cc013004c89b1babe6d779ee043652519c586453bcde761bd7a4b04",
preview.RecipeFingerprint)

retry, err := vault.EnsureVisualPreview(t.Context(), receipt.Version.ID)
require.NoError(t, err)
Expand Down