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

Added tiff compression option #738

Merged
merged 24 commits into from
Mar 29, 2017
Merged

Conversation

kristojorg
Copy link
Contributor

I didn't see a work-in-progress branch so I am sending this to master for now. It doesn't introduce any breaking change in api in any case.

It is actually not totally working yet, and I'm wondering if you can help me identify why. My guess is that I'm setting the wrong option here: this.options.tiffCompression = options.compression;, as I just followed the pattern of similar functions. Where can I find the correct property name to set, and in what format does the input option need to be? I followed what I had used through the vips CLI: 'lzw', 'deflate', and 'jpeg'.

Additionally, I'm not exactly sure what to assert for in a test to determine if this is working? I would assume the compression info must be encoded somewhere, but don't see that as accessible through vips.

If you can help me out with what I am doing wrong, and a testing strategy I will update the PR and add documentation for the feature too. Thanks!!

@kristojorg
Copy link
Contributor Author

My guess is that I need to change this line to support the options, but am not super sure about the proper syntax:

->set("compression", VIPS_FOREIGN_TIFF_COMPRESSION_JPEG));

@kristojorg
Copy link
Contributor Author

Okay I mucked around a bit and it looks like this is working now. Still not exactly sure how to test the compression is actually compressing...

Also not entirely sure how to tiff predictor works, but it does seem to help significantly in reducing file size. Multiple .TIF files around 27MB are reduced to ~20MB without predictor and ~10MB with horizontal predictor.

@coveralls
Copy link

Coverage Status

Coverage increased (+0.008%) to 99.371% when pulling 9173913 on kristojorg:tiff-compression into 9707f8c on lovell:master.

@lovell
Copy link
Owner

lovell commented Mar 21, 2017

Hello, thanks for this PR, a quick glance suggests it's a very good start. I'll make some time in the next day or so to take a more detailed look.

lib/output.js Outdated
@@ -211,6 +211,8 @@ const webp = function webp (options) {
* @param {Object} [options] - output options
* @param {Number} [options.quality=80] - quality, integer 1-100
* @param {Boolean} [options.force=true] - force TIFF output, otherwise attempt to use input format
* @param {Boolean} [options.compression='jpeg'] - compression options: lzw, deflate, jpeg
* @param {Boolean} [options.compression='jpeg'] - compression predictor options: none, horizontal, float
Copy link
Owner

@lovell lovell Mar 22, 2017

Choose a reason for hiding this comment

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

Didi you mean options.predictor here?

src/pipeline.cc Outdated
@@ -1199,6 +1200,25 @@ NAN_METHOD(pipeline) {
baton->webpLossless = AttrTo<bool>(options, "webpLossless");
baton->webpNearLossless = AttrTo<bool>(options, "webpNearLossless");
baton->tiffQuality = AttrTo<uint32_t>(options, "tiffQuality");
// tiff compression options
std::string tiffCompression = AttrAsStr(options, "tiffCompression");
Copy link
Owner

Choose a reason for hiding this comment

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

Given we're going for a string-based input here, can we use vips_enum_from_nick for compression and predictor? (like the kernel property of the resize operation.)

Copy link
Contributor Author

@kristojorg kristojorg Mar 23, 2017

Choose a reason for hiding this comment

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

Okay yea this looks like a better idea. Could you show me where I define the map from nickname -> type though? I can't seem to find it for the example you gave, so I'm wondering how the function takes the string input and decides which type to use?

test/unit/io.js Outdated
@@ -861,6 +861,54 @@ describe('Input/output', function () {
});
});

it('TIFF lzw compression does not throw error', function () {
Copy link
Owner

Choose a reason for hiding this comment

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

Thanks for adding some failure scenarios to the tests. Looking forward to seeing some more positive scenarios/results too.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Could you suggest a strategy for the positive tests? I don't want to simply test that the file got smaller, since occasionally that doesn't work (16 bit tiff files with zip lzw have issues sometimes). Is there a way to read from the metadata that a tiff is compressed?

Copy link
Owner

Choose a reason for hiding this comment

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

I don't want to simply test that the file got smaller, since occasionally that doesn't work

Ah, that's along the lines of what I was thinking. Feel free to add a test fixture that can be relied upon to do this. I guess we can at least verify that the TIFF file(s) can be re-opened.

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 guess we can test that the file got smaller for the testing image, I'll add that.

@kristojorg
Copy link
Contributor Author

I added a positive test, but not sure it works at the moment. Need to fix the vips_enum_from_nick thing in order to get this working again. Let me know how to use that function and I'll get it set up. Thanks!

src/pipeline.cc Outdated
@@ -1199,6 +1200,27 @@ NAN_METHOD(pipeline) {
baton->webpLossless = AttrTo<bool>(options, "webpLossless");
baton->webpNearLossless = AttrTo<bool>(options, "webpNearLossless");
baton->tiffQuality = AttrTo<uint32_t>(options, "tiffQuality");
// tiff compression options
VipsForeignTiffCompression tiffCompression = static_cast<VipsForeignTiffCompression>(
Copy link
Owner

Choose a reason for hiding this comment

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

The following might work:

baton->tiffCompression = static_cast<VipsForeignTiffCompression>(
  vips_enum_from_nick(nullptr, VIPS_TYPE_FOREIGN_TIFF_COMPRESSION,
    AttrAsStr(options, "tiffCompression")));

@kristojorg
Copy link
Contributor Author

Okay I added the positive test and started using vips_enum_from_nick for both tiffCompression and tiffPredictor. Let me know if this works

@lovell
Copy link
Owner

lovell commented Mar 24, 2017

I think you'll need to add the default values for tiffCompression and tiffPredictor to the options near constructor.js#L171 to prevent "undefined" making its way through as a value.

yarn.lock Outdated
@@ -0,0 +1,4467 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
Copy link
Owner

Choose a reason for hiding this comment

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

Was the inclusion of this file intentional?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, no it wasn't. I'll remove that.

@@ -84,6 +84,7 @@ module.exports = {
inputWebPWithTransparency: getPath('5_webp_a.webp'), // http://www.gstatic.com/webp/gallery3/5_webp_a.webp
inputTiff: getPath('G31D.TIF'), // http://www.fileformat.info/format/tiff/sample/e6c9a6e5253348f4aef6d17b534360ab/index.htm
inputTiffCielab: getPath('cielab-dagams.tiff'), // https://github.com/lovell/sharp/issues/646
inputTiffUncompressed: getPath('uncompressed_tiff.tiff'),
Copy link
Owner

Choose a reason for hiding this comment

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

What are the origins/rights etc. of this image?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just a photo I took and have been using for testing myself. Would you prefer a different?

Copy link
Owner

Choose a reason for hiding this comment

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

As long as you're happy to licence it, perhaps using CC0, that's OK.

If possible, please could you use a smaller file, or perhaps only a portion of this file, as this is 15MB!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure exactly what that would mean. Do you have a place you suggest I look for a licensed tiff?

Copy link
Owner

Choose a reason for hiding this comment

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

https://code.google.com/archive/p/imagetestsuite/wikis/TIFFTestSuite.wiki includes 20 images tagged as "Compression Scheme: None" so might provide something suitable. Your own image is fine as long as you grant us permission to distribute it :)

test/unit/io.js Outdated
it('TIFF lzw compression shrinks test file', function (done) {
let startSize;
sharp(fixtures.inputUncompressedTiff)
.toBuffer((err, buffer, info) => {
Copy link
Owner

Choose a reason for hiding this comment

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

Only one toBuffer is supported but there are two here. Perhaps use fs.statSync to read startSize.

test/unit/io.js Outdated
@@ -863,11 +863,11 @@ describe('Input/output', function () {

it('TIFF lzw compression shrinks test file', function (done) {
let startSize;
fs.statSync(fixtures.inputUncompressedTiff, (err, stats) => {
Copy link
Owner

Choose a reason for hiding this comment

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

Did you mean:

const startSize = fs.statSync(fixtures.inputUncompressedTiff).size;

test/unit/io.js Outdated
@@ -861,6 +861,70 @@ describe('Input/output', function () {
});
});

it('TIFF lzw compression shrinks test file', function (done) {
const startSize = fs.statSync(fixtures.inputUncompressedTiff).size;
Copy link
Owner

Choose a reason for hiding this comment

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

The variable that you created is called inputTiffUncompressed not inputUncompressedTiff.

Please, please run the tests locally and ensure they are passing before pushing further code changes to Github.

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 have it working on my end, so I was only getting up on a Saturday to make the tests work to enrich your library. I apologize if it annoyed you.

Feel free to fix it if you want it.

Copy link
Owner

Choose a reason for hiding this comment

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

Thank you. I hope you understand that PRs where tests are failing cannot be merged and code that is untested cannot be safely maintained, refactored or otherwise improved.

Copy link
Owner

Choose a reason for hiding this comment

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

I've just re-read my original comment and it makes me sound so much more annoyed than the slight level of frustration I was experiencing at that point. I'm sorry for any ill feelings it caused to you. Perhaps some form of pre-push hook to run the tests might help here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's alright, sorry I got frustrated as well. It's my second ever PR and I just wasn't thinking about the courtesy and notifications you were probably getting. I'll try to get the tests working and then push it. Thanks for the great lib.

@kristojorg
Copy link
Contributor Author

Alright I have run tests on my machine 😏 and added positive tests for other compression options. All tests are passing at this point. I also switched out the uncompressed tiff file for one from the google library you recommended, as I just didn't want to deal with licensing mine.

The one strange thing I am seeing is that compressing a tiff with the VipsForeignTiffPredictor set to VIPS_FOREIGN_TIFF_PREDICTOR_FLOAT is outputting an all-black image for me. You can see the input and output files here for a deflate compressed tiff. I don't think this has to do with sharp but rather vips. It might not even be an error, I'm unsure.

@coveralls
Copy link

coveralls commented Mar 28, 2017

Coverage Status

Coverage increased (+0.01%) to 99.373% when pulling e728e79 on kristojorg:tiff-compression into 9707f8c on lovell:master.

1 similar comment
@coveralls
Copy link

Coverage Status

Coverage increased (+0.01%) to 99.373% when pulling e728e79 on kristojorg:tiff-compression into 9707f8c on lovell:master.

@lovell lovell merged commit f8e72f4 into lovell:master Mar 29, 2017
@lovell
Copy link
Owner

lovell commented Mar 29, 2017

This is fantastic Kristo, thank you for persisting with this PR, it will make a great addition to sharp.

I was able to reproduce the "float" predictor problem using the vips command line tools so let's report this upstream.

@lovell lovell added this to the v0.17.3 milestone Mar 29, 2017
@kristojorg
Copy link
Contributor Author

Thanks for putting up with me : )

@lovell
Copy link
Owner

lovell commented Mar 29, 2017

Running man tiffcp reveals:

A value 3 is for floating point predictor which you can use if the encoded data are in floating point format.

Running the following at the command line works so libvips is OK.

vips cast uncompressed_tiff.tiff out.tiff[compression=deflate,predictor=float] float

So we'll need to modify sharp to cast pixel values to float when using predictor=float. I'll fix this :)

lovell added a commit that referenced this pull request Mar 31, 2017
@lovell
Copy link
Owner

lovell commented Apr 1, 2017

TIFF float predictor support added in 088d36b, thanks again!

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.

3 participants