A FIFO can have more than one path now: a route after its name, in the syntax of URLPattern. The first routes give the icon at each width, and the images of each FIFO with the options of Cloudflare Images.
Routes for a held FIFO
Yesterday a cache came in front of the Worker, for the images that the VM will make. Today the first one has a path: /images/width=192/favicon.svg gives the icon of mkfifo.com as a PNG of 192 pixels. images is a FIFO, and the path after its name is a route.
A path after the name
Until today a FIFO had one path: /NAME. Now the To of a holder can have a path after the name of the FIFO that it holds:
To: images/width=:width([1-9]\d{0,2})/favicon.svg
X-HTTP-Method-Override: ^GET$
A GET of /images/width=192/favicon.svg is a Request to the FIFO images, and the route gives the capture width. The answer envelope draws the SVG of favicon.svg with $(resvg -w $width - -c < favicon.svg). The answer has Cache-Control: public, max-age=86400, so the cache keeps each width, and the VM draws each width one time. A width of 0, of more than 999, or with a 0 before it gets 404.
Each holder can have routes, not only the FIFOs of the site. The holder shop-k7f2 can save To: shop/:item, and then GET /shop/apple is a Request to shop with $item as apple.
The first plan was the syntax of Cloudflare Pages: [name] for a segment, and [[path]] for the rest of the path. But the other lines of an envelope have patterns, and a group of a route can have one too. The web already has a syntax for this: URLPattern, from the URL Pattern Standard of WHATWG. It has :name, :name(PATTERN), and ?, * and + after a group. Each browser and Node.js has it, so you can try a route in the console of a browser.
The same match as URLPattern
FIFO.Route follows the algorithms of the standard: it reads the route into tokens, makes the parts, and makes the same regular expression. The pattern of a group is a pattern of re, as each pattern of an envelope.
I added some rules. Each group must have a name, because a group is a capture: the standard gives a number to * and to (PATTERN) with no name. And URLPattern changes some text before the match. It removes a segment .. and the segment before it, also %2e%2e, and it encodes a space as %20. Then the text that a person writes and the text of the match are different. So the text of a route can have only the characters that a URL keeps in its path, and no segment . or ... A path with such a segment agrees with no route.
A model, and a check against Node.js
The TLA+ model specs/Routes.tla has routes of whole segments: fixed text, and groups with each modifier, with a pattern for all text or only for digits. For each route and path, the model gives the match. Each part takes a count of segments, and the regular expression of the standard tries the larger count first, from the left. Thus the match has the greatest list of counts, in the order of a dictionary. TLC checks four invariants, and writes 44,440 cases.
scripts/routes.sh gives each case to FIFO.Route and to URLPattern of Node.js, and compares the two results with the result of the model. Then it makes 5,000 random routes with prefixes, suffixes, { }, patterns and modifiers, and 1,000 routes with one character less, which are often errors. For each one, FIFO.Route and URLPattern must give the same groups, or both must refuse the route. To test the check, I changed the regular expression of ? to the lazy ??: the check found 933 differences. The CI runs the check on each push.
Each envelope decides
More than one route can agree with one path. I did not add an order of routes. Each ingress envelope examines the Request, as before, and each egress envelope decides what it sends, and where. The first answer egress gives the answer to the client.
An envelope with no route agrees only with /NAME, so the envelopes of the FIFOs before today do not get the new paths. An email, a Request from a different FIFO and a letter of a page have no path, so they agree only with an envelope with no route. For this reason an envelope with a route gives no letter on the page of a FIFO.
The editor shows the path of each Request. The path of a request that fails is also in the log, so do not put a secret in a route.
A number too large for a float
The full tests of this change found a different problem, in a test. A property test compares the errors of the JSON tokenizer of the selector lines with JSON.decode/1 of Elixir, with random texts. One run made 100e911. JSON.decode/1 refuses it, because no float has this value. But it is valid JSON, and the value of a number in a selector line is its text, so the tokenizer keeps it. The tokenizer was correct, and the test was not. Now the test expects no error for a number out of the range of a float.
The images of each FIFO
/images/width=192,format=webp/NAME now gives the image of the FIFO NAME, 192 pixels wide, as a WebP. The options are the options of Cloudflare Images, but the VM makes the image, and the cache in front of the Worker keeps it for one day. Each FIFO that gives an image to a GET can be a source.
The options of Cloudflare
The new builtin image takes the text of the options of a URL of Cloudflare Images as its word:
$(image width=192,height=192,fit=cover,gravity=top,format=webp < photo)
It has width, height, fit (scale-down, contain, cover, crop, pad and squeeze), gravity, format, quality, background, dpr, rotate and flip, also the short names w, h, f, q and g. It reads a PNG, a JPEG, a WebP or the first frame of a GIF, and writes a PNG, a JPEG or a lossless WebP. Each builtin before it has the name and the options of a tool of a shell. This one has the options of a URL, so that the same text works in a URL and in an envelope.
Some options of Cloudflare give an error. format=auto selects the format from the field Accept of the browser, but the key of the cache has no header field: two browsers would share one copy. avif needs an encoder that the NIF does not have, and the builtin has none of the effects, as blur, sharpen and trim.
A read from a variable
The envelope of /images is short:
To: images/:options/:src([^\/]+(?<!\.svg))
X-HTTP-Method-Override: ^GET$
To: -
Content-Type: image/*
Cache-Control: public, max-age=86400
$(image $options < $src)
< $src is new: it reads the FIFO whose name is the value of a variable. Until now the target of a redirection was a name in the text, because it is an edge of the graph, and the save refuses an edge that makes a loop. A variable has no name at the time of the save, so the save cannot check it. Thus only an envelope of the site can read the FIFO of a variable, and the limit of 8 hops stops a loop at the time of the Request. A script of sh can do it too, because a script never had edges.
Content-Type: image/* is new too. The format of the output can change with the options and with the source, and a variable in Content-Type is an error, because a value of a Request could make an answer of HTML. Now the answer gets the type from the first bytes of the body: image/png, image/jpeg, image/webp or image/gif. Any other body makes the egress fail, so this answer is never HTML or SVG.
A resize with no SIMD
The image comes from the crate image of Rust, in WebAssembly, as the NIF of resvg. My first resize was the crate fast_image_resize, and beam.com did not load the module: "v128 value type requires simd feature". The crate has SIMD code for wasm32, and the WebAssembly of beam.com has no SIMD. The resize of the crate image works, but it keeps a buffer of floats, 16 bytes for each pixel, and the memory of WebAssembly does not get smaller.
So the NIF has its own resize. For a factor of 2 or more, a box filter shrinks the image first, as the shrink of libvips. Then a Lanczos3 filter gives the exact size, with weights of 14 bits, on pixels with the alpha channel multiplied, so a transparent pixel gives no color to the pixels near it. A test compares it with the Lanczos filter of ImageMagick: for a factor below 4, the mean difference is less than one level of 255, and for a factor of 11 it is 2.1, because of the box filter.
The cost of an image
The DO has one thread, so a long image stops each other Request. The NIF reads only the header of the image first, and counts the steps of the work before it starts: decode, resize, crop and encode. I set the steps from the time of each part, so that a step of image takes about the time of a step of resvg. The encoder of PNG was the slowest part. With the Paeth filter in place of adaptive filters, a PNG is 1 to 2% larger, and its encode is 25 to 30% faster.
An image of 1200 by 630 pixels to a WebP 600 pixels wide needs about 4,700,000 steps, less than half of the 10,000,000 that a Request has. A photo of 1920 by 1008 pixels to a width of 800 needs about 16,600,000 steps, so it gets no image. The largest work keeps the memory of the NIF at 23 MiB.