Skip to content

feat(webdav): extract thumbnails through Tika - #3332

Draft
dschmidt wants to merge 5 commits into
imagorfrom
feat/thumbnails-raw-embedded-preview
Draft

dschmidt wants to merge 5 commits into
imagorfrom
feat/thumbnails-raw-embedded-preview

Conversation

@dschmidt

@dschmidt dschmidt commented Aug 18, 2026 •

Copy link
Copy Markdown
Contributor

Thumbnails for everything webdav should not decode in-process: raw camera images, audio cover art, the rendered first page of a pdf, the metafile an office document carries, the icon of a windows binary, and whatever else Tika learns next. One generic step, no format-specific code here.

webdav PUTs the file to Tika's /unpack/preset/thumbnail and hands the image on undecoded, because the generator reads formats webdav cannot. Needs Tika >= 4.1 with the thumbnail preset active in its config ({"presets": {"thumbnail": true}}, TIKA-4856): the preset ships inert, and until a config names it the route answers 404.

The thumbnail is written in the format the image carries, mapped through OC_THUMBNAIL_FORMATS. Its default writes an icon as a PNG, since no generator writes icons back and the fallback is JPEG, which has no alpha channel and would turn a transparent icon black. A file that carries no thumbnail answers 404 rather than 500, so clients cache it as "no preview" instead of retrying.

Config: OC_TIKA_URL, OC_TIKA_THUMBNAIL_MIME_TYPES, OC_THUMBNAIL_FORMATS.

Verified end to end against apache/tika:4.2.0-SNAPSHOT: a pdf renders its first page, an mp3 yields its cover, a windows .exe and .dll yield their icon with transparency intact, an mp3 without cover answers 404. Sharp icons also want apache/tika#3309, which orders an icon group largest first; without it the entry a decoder picks is whatever the resource compiler happened to write first.

What a client is told it can preview still comes from the static mime list, so the formats above are reachable by direct request but not advertised yet. #3210 answers that from the index.

Stacked on #3397.

@codacy-production

codacy-production Bot commented Aug 18, 2026 •

Copy link
Copy Markdown

Not up to standards ⛔

🔴 Issues 3 critical · 1 minor

Alerts:
⚠ 4 issues (≤ 0 issues of at least minor severity)

Results:
4 new issues

Category Results
Security 3 critical
CodeStyle 1 minor

View in Codacy

🟢 Metrics 94 complexity

Metric Results
Complexity 94

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@dschmidt dschmidt changed the title feat(thumbnails): serve the embedded jpeg preview of nikon raw files feat(thumbnails): serve the embedded jpeg preview of raw files Aug 18, 2026
@dschmidt
dschmidt marked this pull request as ready for review August 18, 2026 12:58
@dschmidt
dschmidt force-pushed the feat/thumbnails-raw-embedded-preview branch 2 times, most recently from 4043342 to 47a7136 Compare August 19, 2026 05:31
@dschmidt
dschmidt requested a balanced review from Copilot August 19, 2026 08:53

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds embedded JPEG preview extraction for TIFF-based camera RAW thumbnails.

Changes:

  • Adds RAW MIME routing and documentation.
  • Implements bounded TIFF/BigTIFF preview extraction with EXIF orientation.
  • Adds extraction, hardening, orientation, and decoder tests.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
services/thumbnails/README.md Documents RAW thumbnail support.
pkg/thumbnail/mimetypes.go Registers RAW MIME types.
pkg/thumbnail/mimetypes_vips.go Registers RAW types for VIPS.
pkg/service/grpc/v0/service.go Handles missing RAW previews quietly.
pkg/preprocessor/rawtiff.go Extracts and validates embedded JPEGs.
pkg/preprocessor/rawtiff_test.go Tests RAW extraction and hardening.
pkg/preprocessor/preprocessor.go Routes RAW types to the decoder.
pkg/preprocessor/preprocessor_test.go Tests RAW MIME routing.
pkg/errors/error.go Adds the missing-preview error.
Suppressed comments (4)

services/thumbnails/pkg/preprocessor/rawtiff.go:142

  • This bounds check can overflow before it checks the untrusted BigTIFF offset. For example, a first-IFD offset near MaxUint64 makes ifdOffset + 8 wrap below dlen, after which the slice on line 147 panics. Use subtraction-based bounds checks (or explicit overflow checks) before slicing so a crafted raw upload cannot crash the service.
		if ifdOffset == 0 || ifdOffset+uint64(countSize) > dlen {

services/thumbnails/pkg/preprocessor/rawtiff.go:204

  • arrayOffset is file-controlled, so both additions here can wrap. With an offset near MaxUint64, pos + offW becomes a small number and passes the check, then data[pos:] panics. Reject offsets using subtraction/overflow-safe bounds checks before reading the SubIFD array.
					pos := arrayOffset + j*uint64(offW)
					if pos+uint64(offW) > dlen || len(queue) >= maxIFDs {

services/thumbnails/pkg/preprocessor/rawtiff.go:117

  • BigTIFF widens the entry value field, but accepted LONG values are still 4 bytes. Reading every BigTIFF field as Uint64 misreads a big-endian inline LONG as value << 32; the 8-byte stride used later also misreads LONG SubIFD lists. Decode values and array strides from typ, not only from the container, otherwise valid big-endian BigTIFF/DNG previews are missed.
	// readOff reads a 4- or 8-byte offset/value depending on the container.
	readOff := func(b []byte) uint64 {
		if bigTiff {
			return order.Uint64(b[:8])
		}
		return uint64(order.Uint32(b[:4]))
	}

services/thumbnails/pkg/preprocessor/rawtiff.go:261

  • Returning true immediately on a DCT SOF marker does not establish that the JPEG is renderable: the SOF length may be invalid, or the stream may be truncated before any scan. Because extraction keeps only the largest candidate, a larger corrupt candidate can then mask a valid smaller preview and make Convert fail instead of falling back. Validate enough of the stream to reject malformed candidates, or try candidates in size order until one decodes.
		case 0xc0, 0xc1, 0xc2: // baseline, extended sequential, progressive
			return true

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +57 to +60
// isLongType reports whether a tag type carries a 32-bit (or, in BigTIFF, a
// 64-bit) integer offset we can follow.
func isLongType(typ uint16, bigTiff bool) bool {
return typ == tiffTypeLong || (bigTiff && typ == tiffTypeLong8)
dschmidt added a commit that referenced this pull request Aug 19, 2026
… width

Review of #3332 surfaced two BigTIFF-only defects in the embedded-preview
walker (attacker-controlled input):

- 64-bit IFD and SubIFD-array offsets were bounds-checked with `off + n > len`,
  which wraps for a crafted offset near 2^64, bypassing the guard and slicing
  out of range -> panic (recovered by the framework into a 500 + stack log on
  every crafted request, defeating the no-panic goal). Now overflow-safe.
- a tag value was always read as an 8-byte Uint64 in BigTIFF; a LONG (4-byte)
  offset in a big-endian BigTIFF was thereby shifted. Read it at its declared
  type width instead.

Adds tests for the overflow paths (assert ErrNoImageFromRawFile, no panic),
the big-endian LONG offset, and strengthens the truncation test to assert the
error. Trims a few over-long comments.
@dschmidt
dschmidt requested a balanced review from Copilot August 19, 2026 09:38

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.

Suppressed comments (3)

services/thumbnails/pkg/preprocessor/rawtiff.go:201

  • SubIFD pointers may use TIFF type IFD (13) and BigTIFF type IFD8 (18), but isLongType accepts only LONG/LONG8, so valid preview-bearing SubIFDs using the pointer-specific types are skipped. Extend the type-width/read helpers to accept IFD/IFD8; otherwise such DNG/BigTIFF files incorrectly report no preview.
			case tiffTagSubIFDs:
				if !isLongType(typ, bigTiff) {
					continue

services/thumbnails/pkg/preprocessor/rawtiff.go:221

  • In BigTIFF the value-or-offset field is 8 bytes, so a SubIFDs value containing exactly two LONG offsets is stored inline. Treating every count > 1 value as an array pointer interprets those two offsets as one bogus uint64 file offset and misses both preview IFDs.
				// count > 1: the value field points at an array of count offsets,
				// each of the tag's own width (LONG 4B, LONG8 8B)
				arrayOffset := readOff(entry[valAt:])

services/thumbnails/pkg/preprocessor/rawtiff.go:196

  • Valid TIFF encodes both StripOffsets and StripByteCounts as either SHORT or LONG, but these guards reject SHORT. A raw file whose only embedded JPEG is a single strip using SHORT values will therefore produce ErrNoImageFromRawFile. Please read scalar SHORT values as 16-bit values for both tags (and add a regression case).
				if isLongType(typ, bigTiff) && count == 1 {
					stripOffset = readVal(entry[valAt:], typ)
				}
			case tiffTagStripByteCounts:
				if isLongType(typ, bigTiff) && count == 1 {

@rhafer rhafer left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As much as I'd like to have previews for my raw images (DNG in my case), I'd really prefer us to get not into the business of creating/maintaining a TIFF parser (even with the help of AI). Especially given the experience with the recent security flaws in libvips (which were also TIFF related).

I see your PR to evanoberholster/imagemeta got merged. So using that might help.

And then there is #1128 which suggests to replace the embedded thumbnailer with something like https://github.com/cshum/imagor for better scalability and broader images format support. (Unfortunately AFAICS imagor does not extract the embedded previews though, but tries to render the rawdata)

I am not giving this a 👎 , but I think we should not merge without further consideration.

@butonic any thoughts on this?

@dschmidt

Copy link
Copy Markdown
Contributor Author

Thanks for the considerate review, @rhafer :)

One more idea - tika has a concept of embedded files. I've already implemented audio artwork support using that mechanism.
Currently I'm trying to get the jpeg extraction from raw tiff into tika as well: apache/tika#3037

I mainly wanted both extractions to implement the thumbnails relationship on driveItems, but thinking about it in the context of your concerns and the possible move to imagor it makes me think:

We could pipe audio files and raw images from the thumbnails service to tika for preview extraction.
If that works out well enough, it could be a blueprint for piping regular images to imagor (or to whatever image server else).
This would even rid us of the vendored https://github.com/dhowden/tag (which seems dead and I had to patch and fork for #3208).

Having implemented the original audio artwork thumbnails feature, I can say I'd happily drop the built in support for something more maintained like tika. Moreover, we'd use the same mechanism as in the search service and the supported file formats and features would always align (unlike with a completely different code path like right now).

Thoughts?

@dschmidt

Copy link
Copy Markdown
Contributor Author

I liked my idea so much, that I immediately wanted to give it a spin 😁

+395 now, the built in approach had +936 - I'm satisfied :D
Main downside, we need to wait for the tika upstream pr and a release :)
... but I'm patient, you know ... ;-)

What do you think?

@dschmidt

Copy link
Copy Markdown
Contributor Author

Tika PR is merged 🙂

@dschmidt
dschmidt force-pushed the feat/thumbnails-raw-embedded-preview branch from 4ba384f to 0d0a6a6 Compare August 30, 2026 11:47
@dschmidt dschmidt changed the title feat(thumbnails): serve the embedded jpeg preview of raw files feat(webdav): thumbnails through a Tika server Aug 30, 2026
@dschmidt
dschmidt marked this pull request as draft August 30, 2026 11:48
@dschmidt
dschmidt changed the base branch from main to imagor August 30, 2026 11:49
@dschmidt
dschmidt force-pushed the feat/thumbnails-raw-embedded-preview branch from 0d0a6a6 to 9ac2bc4 Compare August 30, 2026 11:51
@dschmidt dschmidt changed the title feat(webdav): thumbnails through a Tika server feat(webdav): extract thumbnails through a Tika server Aug 30, 2026
@dschmidt
dschmidt force-pushed the feat/thumbnails-raw-embedded-preview branch 4 times, most recently from 05072f1 to 92a183b Compare August 30, 2026 11:57
@butonic
butonic force-pushed the imagor branch 6 times, most recently from 954eb9b to 39569b9 Compare September 4, 2026 21:05
@dschmidt
dschmidt force-pushed the feat/thumbnails-raw-embedded-preview branch from 372bc24 to 7c4ed8a Compare September 6, 2026 19:46
@dschmidt dschmidt changed the title feat(webdav): extract thumbnails through a Tika server feat(webdav): extract thumbnails through Tika Sep 6, 2026
@dschmidt
dschmidt force-pushed the feat/thumbnails-raw-embedded-preview branch from 7c4ed8a to ae5d6fc Compare September 6, 2026 19:54
@dschmidt
dschmidt force-pushed the feat/thumbnails-raw-embedded-preview branch from ae5d6fc to b5fcb61 Compare September 7, 2026 13:32
@butonic
butonic force-pushed the imagor branch 7 times, most recently from 64b25e2 to b306a32 Compare September 9, 2026 05:58
@dschmidt
dschmidt force-pushed the feat/thumbnails-raw-embedded-preview branch 2 times, most recently from 1996387 to 59721bc Compare September 16, 2026 14:12
@dschmidt
dschmidt force-pushed the feat/thumbnails-raw-embedded-preview branch 3 times, most recently from 8061984 to e1da7e8 Compare October 7, 2026 18:27
Every file the built-in converters cannot render goes to /unpack/thumbnail
(Tika 4.1) and comes back with the image Tika marks as its thumbnail.
Tika merged the thumbnail catalog preset and ThumbnailUnpackSelector
(TIKA-4856): PUT /unpack/preset/thumbnail returns exactly the one raster
image a client would show for the document - a stored thumbnail, the
rendering of a metafile thumbnail, or the rendered first page. The
client-side selection over /unpack/all (embedded-resource-type marks,
metafile renderings, id-path matching) moves to the server; the preset
zip carries one image without metadata sidecars, so the entry's
extension names the type and the bytes break the tie. A 404 now points
at the preset activation the server config needs.
The preset ships inert in tika-pipes-core, so PUT /unpack/preset/thumbnail
answers 404 until a config names it, and the thumbnailer then finds no
thumbnail for any document. The acceptance stack and the example
deployment now pass a config that activates it, and the deployment reads
the shared OC_TIKA_URL so the thumbnailer uses the Tika that is already
there. The acceptance image goes to 4.1, the release the preset is in.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants