Summary
Core finding: No image content is sent to the LokalPix server. The server delivers the application and provides aggregate usage statistics; image processing itself takes place on the user's device.
Data flow of a conversion
- 1Load the page
When a converter page loads, the browser may request HTML, CSS, JavaScript, the manifest, icon, and service worker from the same LokalPix origin. The page also reads
/api/stats. The technical documentation page itself loads only HTML, CSS, and, depending on the browser, the icon; it runs no application JavaScript. - 2Select files
Files are selected through the browser's native file picker. JavaScript receives only the files selected by the user.
- 3Validate locally
File type, file size, and image dimensions are checked in the browser. For SVG, the application searches for defined risky elements and patterns and rejects the file if it finds one.
- 4Decode locally
The browser decodes standard formats through
createImageBitmapor a localImageobject. The self-hosted LibRaw WebAssembly library handles DNG and other camera RAW formats; the likewise self-hosted libheif WebAssembly library handles HEIC/HEIF. These large decoders are loaded only when needed. Image data is not transferred to a server address during this process. - 5Draw and encode again
The image is drawn to a canvas.
canvas.toBlob()creates PNG, JPG, or WebP output. A white background is inserted before JPG encoding. - 6Report an aggregate image count
After at least one successful output, the browser sends
POST /api/converta small JSON message containing only the number of successfully converted images. The server accepts integer values from 1 to 20 and adds the value to the aggregate image counter. Image content, file names, formats, sizes, and conversion settings are not sent. - 7Download
The output file is exposed through a temporary blob URL in the browser. It is saved to the device only when the user chooses “Download”.
- 8Release resources
Removing an item or fully resetting the tool revokes the associated blob URLs. Old output URLs are revoked after a successful reconversion or format change. Revocation is also registered for
beforeunload, although that event is not guaranteed to run in every browser lifecycle.
Image processing and formats
Inputs
Accepted inputs are JPEG/JPG including JFIF, PNG, WebP, GIF, BMP, HEIC/HEIF, DNG, CR2, CR3, NEF, ARW, RAF, RW2, ORF, PEF, SRW, ERF, SVG, and AVIF, up to 75 MB per file. At most 20 files enter the queue at once. An empty or generic MIME value may be supplemented from a known file extension; an explicitly unrelated MIME type is not overridden by the extension.
JFIF, BMP, and GIF
JFIF is not a separate image format but the most common container variant of JPEG. Files ending in .jfif, .pjp, and .pjpeg are therefore mapped to image/jpeg and follow the same path as ordinary JPEG files.
BMP (image/bmp, including image/x-ms-bmp and image/x-bmp) and GIF (image/gif) are decoded by the browser. LokalPix first reads dimensions from each file header: the BM marker and following header for BMP, and the GIF87a or GIF89a signature followed by the Logical Screen Descriptor for GIF. If this check fails, the file is rejected before a decoder processes it. No additional library is loaded for JFIF, BMP, or GIF.
Animated GIFs: Output contains only the first frame. This follows the behaviour specified by the HTML Living Standard when an animated image source is drawn to a canvas: the poster frame is used. This route cannot preserve animation in PNG or JPG. See html.spec.whatwg.org, in the section covering canvas image sources.
Local decoders
LibRaw-Wasm develops supported RAW files into RGB pixels. Some newer Apple ProRAW DNG files with JPEG XL compression are not supported by this build. libheif-js decodes HEIC/HEIF files into RGBA pixels. Both libraries and their WebAssembly components are served from the LokalPix origin and run in the browser; no external conversion service is contacted. Output does not reliably retain every EXIF field, depth information, RAW editing latitude, or the source's complete HDR range.
Dimension limits
Each edge is limited to 16,384 pixels and the whole image to 40 megapixels. For PNG, JPEG, WebP, and AVIF, LokalPix attempts to read dimensions from the first 512 bytes before decoding. The limits are checked again after decoding.
Outputs
PNG, JPG, and WebP are generated through the browser's Canvas API. The quality selection affects JPG and WebP. AVIF output is not offered because cross-browser Canvas support is not sufficiently reliable.
Orientation metadata
On the preferred decoding path, createImageBitmap with imageOrientation: 'from-image' asks the browser to apply the orientation stored in the source file. Behaviour on the fallback Image path depends on the browser. EXIF orientation may therefore affect output dimensions and pixels even when the original metadata field is not explicitly copied to the output file.
Special handling for SVG
SVG is an XML-based document format that can contain active or external content. LokalPix parses SVG in the browser and checks a denylist defined in the source code, including scripts, event attributes, embedded foreign content, animation, stylesheets, external href targets, external CSS URLs, CSS imports, expressions, obfuscated style values, and CSS dimension units that cannot be resolved reliably. A match causes rejection; otherwise the serialised SVG structure proceeds to processing. This check reduces risk, but it is not a complete element-and-attribute allowlist and does not guarantee detection of every possible active SVG feature.
Compression
The “Compress JPG” and “Compress PNG” pages use a separate application module, compress-app.mjs. It loads no additional decoders and sends no image data to a server address; the process otherwise follows the conversion flow.
Compression draws the image to a canvas and encodes it again through canvas.toBlob() using the selected quality value. This is lossy re-encoding, not lossless optimisation of existing image data. Image information discarded by re-encoding cannot be recovered.
The quality value affects only JPG and WebP. canvas.toBlob() ignores it for PNG because PNG is lossless. The “Compress PNG” page therefore offers JPG and WebP output, not PNG.
The displayed saving compares output and input byte sizes. A saving is not guaranteed: already compressed sources or high quality settings may produce output larger than the input. The interface reports that result rather than presenting it as a saving.
Resizing
The “Resize image” page uses resize-app.mjs. Image content remains on the device here as well.
Target dimensions can be entered in pixels or as a percentage. The aspect ratio can optionally be preserved; in that case, pixel dimensions form a bounding box. Upscaling beyond the original dimensions can be disabled. The same limits as for conversion apply: no more than 16,384 pixels per edge and 40 megapixels, with a minimum of 1 × 1 pixel.
Scaling uses the browser's canvas. The browser chooses the interpolation algorithm, and LokalPix applies no extra sharpening. Result sharpness may therefore differ between browsers.
Format limitation: HEIC/HEIF and camera RAW files are not supported on this page and are rejected with a specific error. Their decoders are deliberately not included. These files can first be converted to PNG or JPG on a converter page and then resized.
Which network requests are made?
Converter pages
GET /Loads the home page and increments the aggregate page counter.
GET /styles.cssLoads the local design.
GET /app.mjsLoads interaction and conversion logic.
GET /converter-core.mjsLoads format, dimension, and file-name logic.
GET /compress-app.mjsLoads only on compression pages and contains their interaction logic.
GET /resize-app.mjsLoads only on the resizing page and contains its interaction logic.
GET /libraw/*Loads when the first supported camera RAW image is processed and decodes it locally.
GET /libheif/libheif-bundle.mjsLoads when the first HEIC/HEIF image is processed and contains the local decoder including WebAssembly.
GET /api/statsReads views today, total views, reported image count, and counter start date.
POST /api/convertAfter a successful batch, reports the integer number of successfully converted images, from 1 to 20, and the fixed converter-page path. The server adds these aggregate values; image data and file characteristics are not sent.
GET /sw.jsChecks for or installs the service worker.
GET /manifest.webmanifestMay be requested by the browser for the installable web app.
GET /icon.svgMay be requested as the website or application icon.
Service-worker installation and repeat requests
During installation, cache.addAll() requests the German and English converter pages, both technical-privacy pages, the legal-notice page, /styles.css, /app.mjs, /converter-core.mjs, /manifest.webmanifest, and /icon.svg. The large RAW and HEIC decoders are excluded and load only for the first matching image. Later static-resource requests may be answered from Cache Storage. Navigations remain network-first and use an available cached copy after a network failure. The exact number of requests therefore depends on the browser, HTTP cache, and service-worker state.
Technical documentation page
A direct visit to this page loads HTML and CSS and, depending on the browser, /icon.svg. The page itself loads neither app.mjs nor /api/stats and does not register a service worker. A service worker already active from a converter page may still handle its navigation.
The application contains no analytics, advertising, social-media, font, or other third-party scripts. Its Content Security Policy permits scripts, styles, and network connections only from the same origin. Previews and results use local blob: or embedded data: sources.
Boundary: Technically necessary operating-system or browser operations, such as DNS resolution or certificate validation, are outside the application logic and are not equivalent to an image upload.
Usage statistics
A browser navigation is counted on the server when a GET request for a German or English converter page contains Sec-Fetch-Mode: navigate and has not first been classified as a known crawler or bot. Ordinary service-worker precaching is not counted. Technical documentation and legal-notice pages are excluded. To keep the public historical total comparable, it continues only for the core converter routes counted before this extension; the new page breakdown covers all converter pages. For this counting, the reviewed Node application code does not access or log the client IP available on the socket, sets no cookies, and stores no device identifier. Every HTTP connection still exposes its source IP at least to the network and hosting stack, whose behaviour must be reviewed separately.
The statistics additionally group counted navigations by page and calendar day, language, and a coarse referral source: direct, internal link, search engine, social platform, other referral, or unknown. A User-Agent is checked only during the request against fixed patterns and counted as a likely browser, known search crawler, other bot/scraper, or unknown. Conversions are assigned to the browser-reported route after it has been checked against the fixed page inventory.
A newly created statistics file initially contains the base counters. After matching visits or conversions it may additionally contain:
- the counter start date,
- the total number of page views,
- page views per calendar day in the Europe/Berlin time zone,
- aggregate views by route, language, source, and client category,
- aggregate crawler/bot requests by route and category, and
- the total number of reported successfully converted images and their route.
Existing statistics files are not fully validated against an external schema; additional fields already present may survive later writes. Structurally unsuitable but parseable detail buckets are replaced with empty valid aggregates on the next matching event. If the file is malformed or unreadable, the store falls back to a fresh in-memory state, and a later successful write may replace the previous file. Persistence is best effort and batches multiple events for up to about two seconds. A write error is caught for the page request, but the updated counter may then exist only in the process and be lost on restart.
The total and start date remain until the statistics file is administratively deleted or replaced. Existing public daily values are reduced to at most 370 date keys; detailed daily aggregates to at most 90 usage days. These are not time-based deletion periods: older values can remain when there have been fewer usage days. On successful persistence, the temporary JSON file is created with 0600 permissions and renamed. Directory permissions, backups, and privileged system access are outside that control. The public endpoint returns only today's views, total views, the aggregate reported-image count, and the start date with Cache-Control: no-store; detailed aggregates are not exposed publicly. The previous conversion-run counter is not adopted as an image count, so convertedImagesTotal starts separately at zero. The historical value of 17 in conversionsTotal counted completed batch-processing runs, not individual images. One run could contain several images. The two values are not comparable and are not added together.
Important: No IP addresses, full User-Agent strings, full referrer URLs, search terms, cookies, or device identifiers are stored. Service-worker precaching does not increment the counter. Headers can be spoofed, so the categories are probabilistic hints rather than proof that a request came from a person, browser, or bot. The statistics detect neither unique visitors nor repeated reloads.
For POST /api/convert, the server normally uses the IP address visible on the socket solely as an in-memory rate limit key. Only when TRUST_PROXY=1 is explicitly enabled does it use the last entry from X-Forwarded-For; the reverse proxy must set that value reliably and the public must not be able to bypass it by reaching the Node port. The limit counts reported images rather than requests and allows no more than 60 per minute per key. Periodic cleanup can leave a key in process memory for approximately two windows. The rate limit key is not stored permanently: it remains only in memory until cleanup and is not written to the statistics file. Only the aggregate total is persisted, not the number reported by an individual request. Network and hosting layers may independently process connection data.
Measurement limitation: The image count relies on unauthenticated browser reports without cryptographic proof. The strict JSON shape, body-size limit, and rate limit discourage abuse but cannot prevent deliberately manipulated reports. The number is therefore a public usage indicator, not an audit-grade measurement.
What is stored where?
On the server
Static application files and the aggregate statistics file. No selected images and no converted images.
In browser-managed local storage
Selected files or file references, validated SVG copies, previews, canvas contents, and output blobs while the application instance runs. Depending on browser and operating system, these data may reside in memory or temporarily on local disk. The application revokes URLs as described above; the browser and operating system control allocation and actual release.
In Cache Storage
The service worker persistently stores application components such as HTML, CSS, JavaScript, the manifest, and icon until browser data are cleared or a LokalPix update replaces the caches. Image files are not in the cache list. On activation, the worker deletes older caches with the lokalpix- and lokalbild- prefixes.
In the downloads folder
Only when the user initiates a download does the browser save the converted file at a location selected by the user or browser.
Two cache layers must be distinguished: the server responds with Cache-Control: no-cache for HTML, public, max-age=3600 for other static files, and no-store for /api/stats. The service worker uses cache-first for precached non-navigation resources.
LokalPix uses neither localStorage, cookies, IndexedDB, nor a user account for conversion. localStorage stores only the optional light/dark theme preference. After a user chooses to share, the native system dialog opens or only the page URL is copied to the clipboard; image files are not shared by this action.
Security measures
- Content Security Policy: Resources are limited to the same origin by default; objects, third-party frames, and third-party form targets are blocked.
- Frame protection:
frame-ancestors 'none'andX-Frame-Options: DENYinstruct supporting browsers to reject embedding in other pages. - MIME protection:
X-Content-Type-Options: nosniffrestricts MIME sniffing, particularly for script and style destinations. - Referrer protection:
Referrer-Policy: no-referrerprevents a referrer URL from being sent. - Permissions: Camera, microphone, geolocation, and the Payment API are disabled through
Permissions-Policy. - File limits: File count, file size, edge length, and pixel count are bounded.
- SVG checks: Files containing risky elements, references, or style values listed in the source code are rejected.
- No image-upload API: Static pages and
/api/statsuseGETorHEAD.POST /api/convertaccepts onlyapplication/jsonof at most 1,024 bytes with exactly one integer field namedcount; the server parses no image content there. Other methods and POST targets return status 405.
Hosting and reverse-proxy layer
The reviewed Node application code does not persist client IP addresses, set cookies, or accept images. For the POST /api/convert rate limit, it reads the IP address supplied by the reverse proxy or visible on the socket and retains it temporarily in process memory. Every connection also reaches a network and hosting stack that can technically process the source IP. A VPS provider, reverse proxy, CDN, firewall, DDoS protection, DNS service, or system logs may process or set additional connection data, headers, or cookies.
Before publication, the following should therefore be documented and reviewed separately:
- the hosting provider and server location,
- whether access and error logs are stored and for how long,
- whether IP addresses can be shortened or omitted entirely,
- applicable processor and subprocessor relationships, and
- how TLS, backups, updates, access rights, and deletion periods are managed.
This boundary is essential: “No image uploads” is an application property. Claims that the entire server environment keeps no logs are possible only after reviewing the specific production environment.
What this architecture does not guarantee
- A compromised browser, malicious browser extension, or compromised device is outside LokalPix's control.
- Browser decoders can contain security defects; dimension limits reduce risk but do not replace browser updates.
- The SVG check is cautious rejection logic, not a formal security certification of every possible SVG input.
- The application code does not copy source EXIF, IPTC, or XMP fields by default; raster output is re-encoded through Canvas. There is no downstream byte-level inspection, browsers may generate their own metadata, and visible or pixel-embedded information remains. Where the interface permits JPEG EXIF preservation, selecting that option deliberately changes this default.
- Output quality depends on the browser, input format, and selected quality value.
- This technical description does not replace a data-protection impact assessment or legal review where one is required for a particular use.
Recommended review steps
- Open the browser network log: Review a first visit with empty browser data, a repeat visit, a direct visit to this technical page, and an offline case separately. Then select and convert an image; no request containing image data should appear during selection or conversion.
- Inspect requests: Only same-origin resources and local
blob:URLs are expected. No third-party domain should appear. - Inspect server routes:
server.mjscontains no upload endpoint. In addition to GET and HEAD, onlyPOST /api/convertexists for the aggregate counter; include the non-persisted in-memory IP rate limit key in the review. - Inspect conversion logic:
public/app.mjsdecodes, draws, and encodes images through browser APIs. - Inspect statistics: Review
stats-store.mjsand the production statistics file for expected fields, unexpected additional fields, file permissions, backups, and administrative deletion. - Inspect the service worker:
public/sw.jsmay precache only application files, never user-selected image files. Review cache persistence and prefix-based cleanup as well. - Inspect production infrastructure: Document reverse-proxy configuration, access logs, TLS, firewall, backups, user permissions, and provider agreements separately.
- Run regression tests:
npm testandnpm run test:e2ecover core behaviour, security boundaries, and browser flows.
server.mjs
stats-store.mjs
public/app.mjs
public/converter-core.mjs
public/compress-app.mjs
public/resize-app.mjs
public/sw.js
test/
scripts/e2e.mjs
Documentation status: September 2026 · Technical description version 1.7