Skip to content
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

Add support for BLP file format #3007

Merged
merged 8 commits into from
Mar 21, 2018
Merged

Add support for BLP file format #3007

merged 8 commits into from
Mar 21, 2018

Conversation

jleclanche
Copy link
Contributor

This actually uses a custom python decoder for dxt1/3/5 which is... silly, given that we added a cpython decoder in #1652. But this is some very old work I happen to be able to backport now, and I just tested to be working, so I really want to land it even if the decoder could be ported to use the bcn decoder for better performance.



Image.register_open("BLP", BlpImageFile)
Image.register_extension("BLP", ".blp")
Copy link
Member

Choose a reason for hiding this comment

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

Hi. Would you be interested in replacing "BLP" with BlpImageFile.format in these two lines, to avoid duplication, as per #1333?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

@jleclanche
Copy link
Contributor Author

@radarhere any idea why i'm getting "Cannot identify..." errors?

def test_load_blp2_dxt1(self):
im = Image.open("Tests/images/blp2_dxt1.blp")
target = Image.open("Tests/images/blp2_dxt1.png")
self.assert_image_similar(im, target.convert("RGBA"), 15)
Copy link
Member

Choose a reason for hiding this comment

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

In my testing, I find that .convert("RGBA") causes an error, and the test passes once it is removed.

@radarhere
Copy link
Member

Adding your plugin to the list in https://github.com/python-pillow/Pillow/blob/master/src/PIL/__init__.py fixes the error for Python 3.

However, in Python 2, the error persists, because of

File "PIL/BlpImagePlugin.py", line 412, in _decode_blp2
    data = b"".join(data)
TypeError: sequence item 0: expected string, bytearray found

CHANGES.rst Outdated
@@ -35,6 +35,9 @@ Changelog (Pillow)
- Docs: Changed documentation references to 2.x to 2.7 #2921
[radarhere]

- Add a loader for the BLP format from Warcraft and World of Warcraft #3007
[jleclanche]

Copy link
Member

Choose a reason for hiding this comment

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

PRs do not usually have their CHANGES entry as part of them - this is inviting merge conflicts for your PR if another PR is merged first. Whoever merges it in will add the entry.

@jleclanche
Copy link
Contributor Author

All issues fixed and ready to merge.

@jleclanche
Copy link
Contributor Author

@radarhere good to go?

@radarhere
Copy link
Member

While I'm able to provide comments and help with problems, I'd like to leave it up to a longer standing member of Pillow team to do the final review and merge.

@jleclanche
Copy link
Contributor Author

Paging @wiredfool @hugovk

@jleclanche
Copy link
Contributor Author

Paging @wiredfool @hugovk again. Can this land please? or is there anything blocking it?

@jleclanche
Copy link
Contributor Author

Since this has been approved, anything blocking merging it? :)
Sorry to keep bugging but I just want to make sure it lands while all this is still fresh in my head. This is a pretty important and long-awaited decoder for the WoW community.

Copy link
Member

@wiredfool wiredfool left a comment

Choose a reason for hiding this comment

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

What is the source/license of the test images? They're not from WoW are they?

Other than that, and the _open issue, I think this is reasonable.

format = "BLP"
format_description = "Blizzard Mipmap Format"

def _open(self):
Copy link
Member

Choose a reason for hiding this comment

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

_open should not be doing the decoding work, it should only be setting up tiles and deferring to a decoder.

See https://github.com/python-pillow/Pillow/blob/master/docs/example/DdsImagePlugin.py

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I know it can be improved using the bcn decoder, but I wanted to at least land the exhaustive tests.

Copy link
Member

Choose a reason for hiding this comment

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

The example above uses the python decoder framework to split the _open and load phases in pure python. It's a upgrade of your old DDS plugin (which was then obsoleted by the C version)

@jleclanche
Copy link
Contributor Author

blp2_dxt1 and blp2_raw are not copyrightable but other than that, the images are © Blizzard Entertainment. They just need a copyright notice, but I wasn't sure where to put it. Is it ok in the Tests/images/blp folder?

@wiredfool
Copy link
Member

We can't use them if they're copyright Blizzard, unless there's a very specific license that does give us that ability. Depending on the actual license, we may need to add them to the extra images in the depends repo rather than in this repo.

@jleclanche
Copy link
Contributor Author

Fair use applies here, as the assets are copyrighted but:

  • They're used for a very limited purpose
  • They are not redistributed (they live in tests)
  • They are meaningless in the format they're used as in the tests

I was careful to avoid picking any asset that could potentially not be usable (logos, recognizable textures, etc). So I think a copyright notice should be enough, but I'm fine moving them to a separate repo if you prefer, just let me know how.

Btw @wiredfool are you still on freenode? I pinged you there but no answer.

@wiredfool
Copy link
Member

So, no license. That's not going to fly with Debian legal (who has patched out an sRGB icc profile because it contained a copyright string), and it's not nearly open enough to distribute.

Fair Use isn't a license, it's a potential defense.

@jleclanche
Copy link
Contributor Author

jleclanche commented Mar 3, 2018

Why are the tests distributed in the packaged versions?

Edit: Either way as I said just let me know how to move them to a separate repo, I'm fine with that.

@wiredfool
Copy link
Member

Tests are distributed in the source version, which is the canonical reference for all of the distro packagers.

There have to be open source licensed images of this format somewhere, or they should be possible to create from non-copyrighted images.

@jleclanche
Copy link
Contributor Author

The two files I mentioned above are non-copyrightable, but I wanted to make sure to have coverage for all the variations of blp1 and blp2 (most of which the various blp encoders on the market can't generate right now). I'd rather move these wherever is more appropriate to avoid having it in the source.

@jleclanche
Copy link
Contributor Author

@wiredfool I'm a little overwhelmed on fixing the _open issue you pointed out. The PIL API isn't exactly obvious... (even though I've written several decoders I still don't understand it). Is this a merge-blocking issue? The plugin works as is.

Regarding the blp files, please advise.

@wiredfool
Copy link
Member

wiredfool commented Mar 3, 2018

Yes. _open shouldn't do anything expensive, including reading the whole file or decoding it. We've come a good way since the original decoders that you wrote.

The example I linked is your original DDS encoder, redone with the pydecoder interface. Unfortunately, the online diff between it and the original got lost in the move and the conversion of the original into a C form.

However, the basics are here:

def _open():
...
        if fourcc == b"DXT1":
            self.decoder = "DXT1"
            codec = _dxt1
        elif fourcc == b"DXT5":
            self.decoder = "DXT5"
            codec = _dxt5
        else:
            raise NotImplementedError("Unimplemented pixel format %r" % fourcc)

        self.tile = [
            (self.decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))
        ]

_open's responsibility is to set self.size, self.mode, and self.tile. It should do as little as possible to that end, and then return.

and one of the sample decoders:

class DXT1Decoder(ImageFile.PyDecoder):
    _pulls_fd = True

    def decode(self, buffer):
        try:
            self.set_as_raw(_dxt1(self.fd, self.state.xsize, self.state.ysize))
        except struct.error:
            raise IOError("Truncated DDS file")
        return 0, 0

If you're reading from the fd yourself, then you want to set the _pulls_fd, and manage the buffer yourself.

self.set_as_raw sets the entire image as a raw byte stream.

In your case, _open should look something like:

     def _open(self):
        self.magic = self.fp.read(4)
        self._read_blp_header()

        if self.magic == b"BLP1":
            decoder = 'BLP1'
        elif self.magic == b"BLP2":
            decoder = 'BLP2'
        else:
            raise BLPFormatError("Bad BLP magic %r" % (self.magic))
        
        self.tile =  [
            (decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))
        ]

(though, I think you'd have to hoist the self.mode logic up from where it is currently)

Then you'd need two pydecoders that basically defer to the _open_blp* functions, and call self.set_as_raw on the returned bytestream.

@jleclanche
Copy link
Contributor Author

This seems a little silly given that BLP is a container format for DXT and JPG in the first place... it should be pulling in the bcn decoder as I said.

Now don't get me wrong, I'm all for this better interface that doesn't fully read the file on open(), but here's the thing: You're one of the only people in the world who understands that API. Pillow, as I've ranted several times in the past, is extremely hard to understand (I say that having contracted on refactoring 10 year old million-line python codebases), with obscure incantations and voodoo attribute names all over the place.

I get that this isn't your fault, but as it is, you're asking me to spend a lot of time understanding and refactoring this BLP plugin, when you can open just about almost any of the other plugins and find a million much worse offenders; and as far as I can see, only two of them use the PyDecoder interface, making example usage extremely sparse. The DXT decoder you linked in example docs does not seem to even work (self.fd isn't a thing on decoders).

Feel free to do this; I made sure to have exhaustive tests so it can be done in the future (as I said, this should use the c decoder, not the python one), and the plugin itself is as clean as I could make it. But I'd like to merge in the current version as soon as you let me know where to push the sample images to.

@wiredfool
Copy link
Member

@jleclanche

That example was the workpiece for the PyDecoder. it was ready to merge when you converted the decoders it was based on to C. I assure you that it works, and some of the changes, such as self.fd, are there specifically because the API was hard to work with.

So, here you go, 1 hour while the kids are having stories. wiredfool@196717f#diff-ba00046598bc3bd2e09dd786d7f74950L252

I'm not 100% happy with it due to code duplication, and the format detection probably could be pushed back into the _open method, and then delegate directly to the raw, jpeg, and other decoders in self.tile.

@jleclanche
Copy link
Contributor Author

@wiredfool Forgot to ping back! Thank you for the help with this. I have rebased it against master.

Now what do we do about the images? If moving them to another repo is a problem then I'll drop the copyrighted ones just to get this landed; but I'd rather they live somewhere.

@jleclanche
Copy link
Contributor Author

@wiredfool poke

@hugovk
Copy link
Member

hugovk commented Mar 12, 2018

@jleclanche Have a look at test_images in https://github.com/python-pillow/pillow-depends and how, for example, the picins dir is skipped https://github.com/python-pillow/Pillow/search?utf8=%E2%9C%93&q=picins&type=.

It would be good to have at least some redistributable ones in this repo.

@wiredfool
Copy link
Member

I've thought about this some more, and I don't think we should be adding images that are Copyright Blizzard in that repo either. The ones that are there are there are included from other test image archives and have unclear licenses or licenses that wouldn't pass Debian Legal. This is a different case, and we shouldn't be hosting these in one of our repos.

@hugovk
Copy link
Member

hugovk commented Mar 12, 2018

Sounds reasonable, then let's try and find or create "free" replacements for the copyright ones.

@jleclanche
Copy link
Contributor Author

I've removed the copyrighted images. The three that remain are good to go.

@jleclanche
Copy link
Contributor Author

Ping for a merge @wiredfool

Copy link
Member

@wiredfool wiredfool left a comment

Choose a reason for hiding this comment

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

LGTM

@wiredfool
Copy link
Member

Still needs documentation and release notes #3018

@wiredfool wiredfool merged commit f504cbe into python-pillow:master Mar 21, 2018
@jleclanche jleclanche deleted the feat/blp branch September 4, 2020 07:45
@radarhere
Copy link
Member

I've removed the copyrighted images. The three that remain are good to go.

@jleclanche I realise it's been a long time since this PR, but you actually have added four images here, not three. Should the fourth one, blp1_jpeg.blp, be removed, or are we ok to use it for testing from a copyright perspective?

@jleclanche
Copy link
Contributor Author

Everything I added here is ok to use yep

@radarhere
Copy link
Member

Thanks very much for the reply.

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.

4 participants