Skip to content

Inverted 1bpp #19

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 33 commits into from
Apr 16, 2022
Merged

Inverted 1bpp #19

merged 33 commits into from
Apr 16, 2022

Conversation

jamwaffles
Copy link
Member

@jamwaffles jamwaffles commented Mar 15, 2022

Thank you for helping out with tinybmp development! Please:

  • Check that you've added passing tests and documentation
  • Add a CHANGELOG.md entry in the Unreleased section under the appropriate heading (Added, Fixed, etc) if your changes affect the public API
  • Run rustfmt on the project
  • Run just build (Linux/macOS only) and make sure it passes. If you use Windows, check that CI passes once you've opened the PR.

PR description

Closes #18.

I wrote this pretty late, so my interpretation of the BMP format might be incorrect. I'd also gladly accept suggestions for improvements in the commented-on areas (see files tab) for performance and cleanliness reasons.

@jamwaffles jamwaffles marked this pull request as ready for review March 15, 2022 23:20
@jamwaffles jamwaffles requested a review from rfuest March 15, 2022 23:20
Copy link
Member

@rfuest rfuest left a comment

Choose a reason for hiding this comment

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

It would be great if we could also support 8bpp images with color tables.

@jamwaffles
Copy link
Member Author

Thanks for the review! I agree with your comments; I'll find some time to refactor the header parsing to do it properly. Should we change the information in the publicly exposed Header, or just discard the extra fields from the various DIB variants?

@rfuest
Copy link
Member

rfuest commented Mar 18, 2022

Should we change the information in the publicly exposed Header, or just discard the extra fields from the various DIB variants?

I think this depends on whether we want tinybmp to be a generic BMP parsing library in addition to the e-g centric use cases. If you intend tinybmp to be as generic as possible we should expose as much header information as possible, otherwise we don't need to store any more information than we use internally.

Some fields in the Header struct aren't necessary IMO. image_data_len, for example, isn't used at all. And image_data_start shouldn't be necessary either, because you can usually use RawData::image_data instead.

One refactoring I had previously considered was to remove all fields from Header and instead use getters which directly access the BMP data:

struct Header<'a> {
    data: &'a [u8],
}

impl Header {
    fn width(&self) -> i32 {
        i32::from_le_bytes(self.data[0x12..0x12+4].try_into().unwrap())
    }
}

Writing the getters would be some effort and not as nice as using nom, but we could add getters without any RAM overhead. The constructor for the Header struct would check for the size of the header and invalid values. In addition to the reduced memory overhead this method would also allow us to later add additional getters as non breaking changes.

@jamwaffles
Copy link
Member Author

I think this depends on whether we want tinybmp to be a generic BMP parsing library in addition to the e-g centric use cases.

A good question, and one I don't really have a solid answer to. My best response is I want this crate to be "useful", so in that vein I think exposing more header data is probably the way to go (but not for this PR).

Some fields in the Header struct aren't necessary IMO. image_data_len, for example, isn't used at all. And image_data_start shouldn't be necessary either, because you can usually use RawData::image_data instead.

Good points, but for this PR, I think we should keep the public API the same and refactor the header parsing as you suggest, even if we discard most of what we parse. This lays the groundwork for changing the API in the future as well as keeping the scope of this PR to its original purpose.

That's an interesting idea for the getter-based API. I do like the cleanliness of getting a big struct of everything though, and the fields are very well defined so we know what to return without having to add breaking changes in the future. We could consider a hybrid approach and have getters for individual header structs instead of fields.

@rfuest
Copy link
Member

rfuest commented Mar 19, 2022

Good points, but for this PR, I think we should keep the public API the same and refactor the header parsing as you suggest, even if we discard most of what we parse. This lays the groundwork for changing the API in the future as well as keeping the scope of this PR to its original purpose.

Agreed, let's keep this PR focused on the original problem.

} else {
// No color mapping - use value directly
pixel_value[0] = *byte
}
Copy link
Member Author

Choose a reason for hiding this comment

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

I'm sure there's a better way to do these indexing operations, but I'm pretty tired at the moment.

Copy link
Member

Choose a reason for hiding this comment

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

As mentioned before, I would not mix the color table lookup and the RawPixels iter. Instead RawPixels would always return the raw image data, which would be the index into the color table for images with a pallete. The iterator could then be mapped to the actual color values by looking up the index in the color table. This step would be optional and shouldn't be used for true color images to improve performance.

Copy link
Member Author

Choose a reason for hiding this comment

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

That makes sense. Are you expecting the colour table logic to go into the Pixels iterator impl? I'm unsure how we'd be able to make the iterator zero cost for images without a colour table. Did you have a particular impl in mind?

Copy link
Member

Choose a reason for hiding this comment

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

The color table logic would need to go into the Pixels iterator impl and it couldn't be zero cost in this instance.

But we don't need to use the Pixels iterator in the ImageDrawable implementation. RawBmp::draw could use RawPixels instead of Pixels and have different code paths for image with or without a color table, like this pseudo code:

    pub(crate) fn draw<D>(&self, target: &mut D) -> Result<(), D::Error>
    where
        D: DrawTarget,
        D::Color: From<<D::Color as PixelColor>::Raw>,
    {
        if has_color_table {
            target.fill_contiguous(
                &Rectangle::new(Point::zero(), self.size()),
                self.pixels().map(|Pixel(_, color)| do_color_mapping(color)),
            )
        } else {
            target.fill_contiguous(
                &Rectangle::new(Point::zero(), self.size()),
                self.pixels().map(|Pixel(_, color)| D::Color::Raw::from_u32(color).into()),
            )
        }
    }

@jamwaffles
Copy link
Member Author

Header parsing has been refactored to handle different DIB types better, although it ignores most fields from them.

Thank you also for showing me length_data - I didn't know it existed and it's a handy lil function!

@jamwaffles jamwaffles requested a review from rfuest March 19, 2022 21:12
} else {
// No color mapping - use value directly
pixel_value[0] = *byte
}
Copy link
Member

Choose a reason for hiding this comment

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

As mentioned before, I would not mix the color table lookup and the RawPixels iter. Instead RawPixels would always return the raw image data, which would be the index into the color table for images with a pallete. The iterator could then be mapped to the actual color values by looking up the index in the color table. This step would be optional and shouldn't be used for true color images to improve performance.

@rfuest
Copy link
Member

rfuest commented Mar 20, 2022

Thank you also for showing me length_data - I didn't know it existed and it's a handy lil function!

It is, I only recently discovered it myself. Really unfortunate that the BMP format makes it harder to use by including the length bytes in the data size.

@rfuest
Copy link
Member

rfuest commented Mar 29, 2022

Rust 1.40.0 doesn't seem to like one of the trait bounds. Do we want to keep the old Rust version or update to the new Rust version and edition 2021?

Edit: see #21 (comment)

@jamwaffles
Copy link
Member Author

I think I've fixed the 1.40 build issue, although I'm on macOS so can't test locally.

I'd like to at least release this PR before we bump to 2021.

One issue I noticed is that we didn't check for invalid color indices if colors_used < (1 << bpp). How should we handle that case?

Let's just abort for now.

As for this PR, let's limit the scope creep and call it done if you're happy with the public API. Internals can be refactored later :)

@rfuest
Copy link
Member

rfuest commented Mar 29, 2022

The problem is that is also kind of broken, e.g. that DynamicBmp doesn't use the color table.

@rfuest
Copy link
Member

rfuest commented Mar 29, 2022

I would be OK with merging this now, but IMO we should fix some issues before the next release.

@jamwaffles
Copy link
Member Author

but IMO we should fix some issues before the next release.

Agreed. I didn't realise DynamicBmp was broken when I wrote my comment. I think it makes sense to fix that in another PR though, and I'll hold off on a release until it's fixed of course.

@jamwaffles jamwaffles mentioned this pull request Mar 29, 2022
@rfuest
Copy link
Member

rfuest commented Mar 30, 2022

Agreed. I didn't realise DynamicBmp was broken when I wrote my comment. I think it makes sense to fix that in another PR though, and I'll hold off on a release until it's fixed of course.

If we want to do one non breaking release before the change to a newer Rust version I would suggest to leave DynamicBmp as it is for that release. The original intent of fixing 1 BPP images doesn't apply to DynamicBmp, because it never supported 1 BPP images. I would like to, instead of fixing DynamicBmp now, clean up the color type mess and probably get rid of DynamicBmp in the next breaking release.

@jamwaffles
Copy link
Member Author

Ok, that sounds like a good approach :)

@rfuest
Copy link
Member

rfuest commented Mar 30, 2022

Hmm, this is even more broken than I noticed before. The color table is always stored as Rgb888 and at the moment we just convert the raw value into C, which works for our examples but not all BMP files. I don't think this is fixable in a non breaking way, because we would at least need to require that C in Bmp implements From<Rgb888>.

New plan: We remove all user visible references to the new color table code and perhaps disable some of the additional checks. Then we ship one last non breaking version that only fixes the original issue. After the release we make the new code accessible to the user and clean up the rest.

@jamwaffles
Copy link
Member Author

I'm sorry this is 15 days late. It's been 15 days of "I don't feel like programming today, I'll do it tomorrow"...

I've pushed a change that un-pubs ColorTable and its getters. I've split the image tests that use the colour table into the color_table module which I'm not too happy about so suggestions welcome.

What checks should we walk back? The ones in the header parse, or the ones that check the bit depth in Bmp::from_slice?

@rfuest
Copy link
Member

rfuest commented Apr 15, 2022

I'm sorry this is 15 days late. It's been 15 days of "I don't feel like programming today, I'll do it tomorrow"...

I hope this wasn't caused by my constant nagging about issues with this PR. But unfortunately this PR uncovered some existing issues that weren't directly related to your contribution in this PR.

I've pushed a change that un-pubs ColorTable and its getters. I've split the image tests that use the colour table into the color_table module which I'm not too happy about so suggestions welcome.

I would suggest to leave the tests in the color_table for now and move it back with the next breaking release, which makes the color table public again. IMO just add a TODO in the code or open a tracking issue for the next breaking release.

What checks should we walk back? The ones in the header parse, or the ones that check the bit depth in Bmp::from_slice?

After taking another look at the checks I believe we can keep them. But the changelog should mention that 1 BPP and 8 BPP images are now required to have a color table.

@jamwaffles
Copy link
Member Author

I hope this wasn't caused by my constant nagging about issues with this PR.

Ah, no, sorry if it came across that way. I was grumbling about my lack of motivation, not being driven away :). The points you've raised so far are all valid so are all reasonable.

I've added a TODO and tweaked the changelog entry a bit. Let me know if there's anything else.

@rfuest
Copy link
Member

rfuest commented Apr 16, 2022

Did you notice this comment https://github.com/embedded-graphics/tinybmp/pull/19/files#r835934583? I would prefer to ignore colors_important, because its value shouldn't matter.

I also noticed that you reordered some of the use statements. Is this the order you prefer or caused by rustfmt? I would like too keep the order consistent across the e-g crates (at least to some extent). I try to maintain the same order as rust-analyzer uses: https://rust-analyzer.github.io/manual.html#auto-import

@jamwaffles
Copy link
Member Author

Did you notice this comment

Thanks for reminding me. I've pushed some changes to completely ignore colors_important. cargo test still passes so I'm guessing it's correct... or our tests are junk :D

I also noticed that you reordered some of the use statements. Is this the order you prefer or caused by rustfmt?

I prefer to clump all imports together with no separating lines, but I've pushed a commit to make this PR's diff smaller in regard to imports. I don't want to bikeshed this too much but I'm happy to accept a PR in the future that unifies the style across tinybmp.

@rfuest
Copy link
Member

rfuest commented Apr 16, 2022

cargo test still passes so I'm guessing it's correct... or our tests are junk :D

Let's just assume the tests are perfect ;)

I prefer to clump all imports together with no separating lines, but I've pushed a commit to make this PR's diff smaller in regard to imports. I don't want to bikeshed this too much but I'm happy to accept a PR in the future that unifies the style across tinybmp.

I don't like to mix crate imports with other imports, but have no preference if std/core imports should be mixed with imports from other crates.

@jamwaffles jamwaffles merged commit 5d3f3ce into master Apr 16, 2022
@jamwaffles jamwaffles deleted the inverted-1bpp branch April 16, 2022 17:15
@jamwaffles
Copy link
Member Author

Thanks for the reviews!

@jamwaffles
Copy link
Member Author

Shall I release 0.3.2 as a new patch release?

@rfuest
Copy link
Member

rfuest commented Apr 16, 2022

Shall I release 0.3.2 as a new patch release?

SGTM.

How do we want to continue after that release? Do you want to work on fixing the color issues for the next breaking release?

@jamwaffles
Copy link
Member Author

That, and adding better error handling as per #20 would be good. I'm focussing on other things at the moment so don't have much time to work on tinybmp but I'm happy to review stuff and help out on smaller stuff.

@rfuest
Copy link
Member

rfuest commented Apr 16, 2022

OK, I'll try to implement these changes if I have some time. It would be good if you could take a look at embedded-graphics/tinytga#16, because the changes to tinybmp would be similar.

@jamwaffles
Copy link
Member Author

The tinytga PR is on my todo list :). I had a quick skim and it looks good so far. I'll take a closer look when I've got a moment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Color palette not parsed correctly for 1BPP images
2 participants