A tile is basically a strip with a user-defined width. With that in
mind, adding support for them is quite straightforward. As a lot the
common code was named after 'strips', to avoid future confusion I
renamed everything that interact with either strips or tiles to a
global term: 'segment'.
Note that tiled images are supposed to always have a 'TileOffsets' tag
instead of 'StripOffset'. However, this doesn't seem to be enforced by
encoders, so we support having either of them indifferently.
The test case was generated with the following Python script:
import pyvips
img = pyvips.Image.new_from_file('deflate.tiff')
img.write_to_file('tiled.tiff',
compression=pyvips.ForeignTiffCompression.DEFLATE,
tile=True, tile_width=64, tile_height=64)
This variable stores the number of rows from the beginning of the image,
contrary to `row` that stores the number of rows relative to the start
of the current segment.
The first marker is always white in CCITT streams, so lines starting
with a black pixel encodes a symbol meaning 0 white pixels. Then, the
decoding would proceed with a black symbol. We used to set the symbol's
color based on `column == 0`, which is wrong in this situation.
The `read_tag()` function is not mandated to keep the reading head at a
meaningful position, so we also need to align the pointer after the last
tag. This solves a bug where reading the last field of an IFD, which is
placed after the tags, was incorrect.
Every TIFF containers is composed of a main IFD. Some entries of this
one can be a pointer to a sub-IFD. We are now capable of exploring these
underlying structures. Note that we don't do anything with them yet.
JPEGStream::byte_offset() now returns an offset relative to the start
of the stream, instead of relative to the buffered part.
No behavior change except if JPEG_DEBUG is set.
We now reject fonts where the active cmap subtable is in a format
we can't read yet, instead of silently drawing squares for all glyphs.
This doesn't fire at all for my 1000-file PDF test set, but seems
like a good thing to check.
(Instead of duplicating the switch, I first tried making a
glyph_id_for_code_point_or_else() that returns ErrorOr<u32> and then
make both glyph_id_for_code_point() and validate_format_can_be_read()
call that, but I liked less how that worked out -- felt too clever.)
This would've saved me some debugging on #23103.
We now return an error instead of a font that draws squares for all
characters. That seems preferable since it makes these cases easy to
find. This fires for three files in my 1000-file PDF test set, so it's
not exceedingly common (...but I wasn't aware that three files were
rendering boxes for this reason, and now I am and can just make them
work in the future).
`x.size()` is 3 or 4 in practice and at most 15 in theory
(cf `number_of_components_in_color_space()` in Profile.cpp),
so using a VLA for these should be fine from a stack size PoV.
It's only accessed through a span, so there's no additional
security risk.
Takes
Build/lagom/bin/image --no-output \
--assign-color-profile \
Build/lagom/Root/res/icc/Adobe/CMYK/USWebCoatedSWOP.icc \
--convert-to-color-profile serenity-sRGB.icc \
cmyk.jpg
from 2.74s to 2.66s on my machine, almost 3% faster.
(Don't do this in LibPDF's SampledFunction::evaluate() since there's
no bound on the dimension of the input function. Realistically,
size of the table puts a pretty low bound on that dimension though,
so we should probably enforce some bound in SampledFunction::create()
and do this there too.)
`x.size()` is 3 or 4 in practice and at most 15 in theory
(cf `number_of_components_in_color_space()` in Profile.cpp),
so using a VLA for these should be fine from a stack size PoV.
They're accessed from two local loops iterating from 0 to
`x.size()`, so it's hopefully not too risky from a security
PoV either.
Takes
Build/lagom/bin/image --no-output \
--assign-color-profile \
Build/lagom/Root/res/icc/Adobe/CMYK/USWebCoatedSWOP.icc \
--convert-to-color-profile serenity-sRGB.icc \
cmyk.jpg
from 2.81s to 2.74s on my machine, about 2.5% faster.
Previously, if we wanted to to e.g. do linear interpolation in 2-D,
we'd get a sample point like (1.3, 4.4), then get 4 samples around
it at (1, 4), (2, 4), (1, 5), (2, 5), then reduce the 4 samples
to 2 samples by computing the combined samples
`0.3 * f(1, 4) + 0.7 * f(2, 4)` and `0.3 * f(1, 5) + 0.8 * f(2, 5)`,
and then 1-D linearly blending between these two samples with the
factor 0.4. In the end we'd multiply the first value by 0.3 * 0.4,
the second by 0.7 * 0.4, the third by 0.3 * 0.6, and the third by
0.7 * 0.6, and then sum them all up.
This requires computing and storing 2**N samples, followed by
another 2**N iterations to combine the 2**N sampls to a single value.
(N is in practice either 4 or 3, so 2**N isn't super huge.)
Instead, for every sample we can directly compute the product of
weights and sum them up directly. This lets us omit the second loop
and storing 2**N values, in exchange for doing an additional O(n)
work to compute the product.
Takes
Build/lagom/bin/image --no-output --invert-cmyk \
--assign-color-profile \
Build/lagom/Root/res/icc/Adobe/CMYK/USWebCoatedSWOP.icc \
--convert-to-color-profile serenity-sRGB.icc \
cmyk.jpg
form 3.42s to 3.08s on my machine, almost 10% faster (and less code).
Here cmyk.jpg is a 2253x3080 cmyk jpeg, and USWebCoatedSWOP.icc is an
mft2 profile with input tables with 256 samples and a 9x9x9x9 CLUT.
The LibPDF change is covered by TEST_CASE(sampled) in LibPDF.cpp,
and the LibGfx change is basically the same change as the one in
LibPDF (where the test results don't change) and the output
subjectively looks identical. So hopefully this causes indeed no
behavior change :^)
bab2113ec1 made read_whitespace() return ErrorOr, which makes this
easy to do.
(7cafd7d177, which added the fixmes, landed slightly after bab2113ec1,
so not quite sure why it wasn't like this immediately. Maybe commit
order got changed during review; both commits were in #17831.)
No behavior change.
We always store CMYK data as YCCK, for two reasons:
1. If we ever want to do subsampling, then doing 2111 or
2112 makes sense with YCCK, while it doesn't make sense
if we store CMYK directly.
2. It forces us to write a color transform header. With a color
transform header, everyone agrees that the CMYK channels should
be stored inverted, while without it behavior between decoders
is inconsistent. (We could write an explicit color transform header
for CMYK too though, but with YCCK it's harder to forget since the
output will look wrong everywhere without it.)
initialize_mcu() grows a full CMYKBitmap override. Some of the
macroblock traversal could probably shared with some kind of
for_all_macroblocks() type function in the future, but the color
conversion math is different enough that this should be a separate
function.
Other than that, we pass around a mode parameter and make a few fuctions
write 4 instead of 3 channels, and that's it.
We use the luminance quantization and huffman tables for the K
channel.
`CMYKBitmap::to_low_quality_rgb()` morally still does the same thing,
but it has a slightly more scary name, and it doesn't use this exact
function. So let's toss it :^)
JPEGs can store a `restart_interval`, which controls how many
minimum coded units (MCUs) apart the stream state resets.
This can be used for error correction, decoding parts of a jpeg
in parallel, etc.
We tried to use
u32 i = vcursor * context.mblock_meta.hpadded_count + hcursor;
i % (context.dc_restart_interval *
context.sampling_factors.vertical *
context.sampling_factors.horizontal) == 0
to check if we hit a multiple of an MCU.
`hcursor` is the horizontal offset into 8x8 blocks, vcursor the
vertical offset, and hpadded_count stores how many 8x8 blocks
we have per row, padded to a multiple of the sampling factor.
This isn't quite right if hcursor isn't divisible by both
the vertical and horizontal sampling factor. Tweak things so
that they work.
Also rename `i` to `number_of_mcus_decoded_so_far` since that
what it is, at least now.
For the test case, I converted an existing image to a ppm:
Build/lagom/bin/image -o out.ppm \
Tests/LibGfx/test-inputs/jpg/12-bit.jpg
Then I resized it to 102x77px in Photoshop and saved it again.
Then I turned it into a jpeg like so:
path/to/cjpeg \
-outfile Tests/LibGfx/test-inputs/jpg/odd-restart.jpg \
-sample 2x2,1x1,1x1 -quality 5 -restart 3B out.ppm
The trick here is to:
a) Pick a size that's not divisible by the data size width (8),
and that when rounded to a block size (13) still isn't divisible
by the subsample factor -- done by picking a width of 102.
b) Pick a huffman table that doesn't happen to contain the bit
pattern for a restart marker, so that reading a restart marker
from the bitstream as data causes a failure (-quality 5 happens
to do this)
c) Pick a restart interval where we fail to skip it if our calculation
is off (-restart 3B)
Together with #22987, fixes#22780.
Non-interleaved files always have an MCU of one data unit.
(A "data unit" is an 8x8 tile of pixels, and an "MCU" is a
"minium coded unit", e.g. 2x2 data units for luminance and
1 data unit each for Cr and Cb for a YCrCb image with
4:2:0 subsampling.)
For the test case, I converted an existing image to a ppm:
Build/lagom/bin/image -o out.ppm \
Tests/LibGfx/test-inputs/jpg/12-bit.jpg
Then I converted it to grayscale and saved it as a pgm in Photoshop.
Then I turned it into a weird jpeg like so:
path/to/cjpeg \
-outfile Tests/LibGfx/test-inputs/jpg/grayscale_mcu.jpg \
-sample 2x2 -restart 3 out.pgm
Makes 3 of the 5 jpegs failing to decode at #22780 go.
That's all this function reads from Component.
Also rename from validate_luma_and_modify_context() to
validate_sampling_factors_and_modify_context().
No behavior change.
These are written by `mutool extract` for CMYK images.
They don't contain color profiles so they're not super convenient.
But `image` can convert them (*) to sRGB as long as you use it with
`--assign-color-profile` pointing to some CMYK icc profile of your
choice.
*: Once #22922 is merged.
.pam is a "portrable arbitrarymap" as documented at
https://netpbm.sourceforge.net/doc/pam.html
It's very similar to .pbm, .pgm, and .ppm, so this uses the
PortableImageMapLoader framework. The header is slightly different,
so this has a custom header parsing function.
Also, .pam only exixts in binary form, so the ascii form support
becomes optional.
It converts from a CMYKBitmap to an (rgb) bitmap, using a real
color profile.
The API design here isn't super scalable (what if we want to also
handle grayscale inputs? What if we also want to convert _to_ cmyk
or grayscale?), but we have to start somewhere. Uses of this can
inform future API improvements.