sharp 0.30.4 → 0.30.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/install/libvips.js +9 -1
- package/lib/composite.js +1 -1
- package/lib/input.js +5 -5
- package/lib/libvips.js +11 -2
- package/lib/output.js +5 -2
- package/lib/platform.js +5 -3
- package/lib/resize.js +1 -1
- package/lib/sharp.js +4 -1
- package/lib/utility.js +17 -4
- package/package.json +7 -6
- package/src/common.cc +5 -2
- package/src/common.h +1 -1
- package/src/pipeline.cc +16 -12
package/install/libvips.js
CHANGED
|
@@ -35,6 +35,7 @@ const hasSharpPrebuild = [
|
|
|
35
35
|
];
|
|
36
36
|
|
|
37
37
|
const { minimumLibvipsVersion, minimumLibvipsVersionLabelled } = libvips;
|
|
38
|
+
const localLibvipsDir = process.env.npm_config_sharp_libvips_local_prebuilds || '';
|
|
38
39
|
const distHost = process.env.npm_config_sharp_libvips_binary_host || 'https://github.com/lovell/sharp-libvips/releases/download';
|
|
39
40
|
const distBaseUrl = process.env.npm_config_sharp_dist_base_url || process.env.SHARP_DIST_BASE_URL || `${distHost}/v${minimumLibvipsVersionLabelled}/`;
|
|
40
41
|
const installationForced = !!(process.env.npm_config_sharp_install_force || process.env.SHARP_INSTALL_FORCE);
|
|
@@ -42,7 +43,9 @@ const installationForced = !!(process.env.npm_config_sharp_install_force || proc
|
|
|
42
43
|
const fail = function (err) {
|
|
43
44
|
libvips.log(err);
|
|
44
45
|
if (err.code === 'EACCES') {
|
|
45
|
-
libvips.log('Are you trying to install as a root or sudo user?
|
|
46
|
+
libvips.log('Are you trying to install as a root or sudo user?');
|
|
47
|
+
libvips.log('- For npm <= v6, try again with the "--unsafe-perm" flag');
|
|
48
|
+
libvips.log('- For npm >= v8, the user must own the directory "npm install" is run in');
|
|
46
49
|
}
|
|
47
50
|
libvips.log('Please see https://sharp.pixelplumbing.com/install for required dependencies');
|
|
48
51
|
process.exit(1);
|
|
@@ -156,6 +159,11 @@ try {
|
|
|
156
159
|
if (fs.existsSync(tarPathCache)) {
|
|
157
160
|
libvips.log(`Using cached ${tarPathCache}`);
|
|
158
161
|
extractTarball(tarPathCache, platformAndArch);
|
|
162
|
+
} else if (localLibvipsDir) {
|
|
163
|
+
// If localLibvipsDir is given try to use binaries from local directory
|
|
164
|
+
const tarPathLocal = path.join(path.resolve(localLibvipsDir), `v${minimumLibvipsVersionLabelled}`, tarFilename);
|
|
165
|
+
libvips.log(`Using local libvips from ${tarPathLocal}`);
|
|
166
|
+
extractTarball(tarPathLocal, platformAndArch);
|
|
159
167
|
} else {
|
|
160
168
|
const url = distBaseUrl + tarFilename;
|
|
161
169
|
libvips.log(`Downloading ${url}`);
|
package/lib/composite.js
CHANGED
|
@@ -50,7 +50,7 @@ const blend = {
|
|
|
50
50
|
* `hard-light`, `soft-light`, `difference`, `exclusion`.
|
|
51
51
|
*
|
|
52
52
|
* More information about blend modes can be found at
|
|
53
|
-
* https://libvips.
|
|
53
|
+
* https://www.libvips.org/API/current/libvips-conversion.html#VipsBlendMode
|
|
54
54
|
* and https://www.cairographics.org/operators/
|
|
55
55
|
*
|
|
56
56
|
* @since 0.22.0
|
package/lib/input.js
CHANGED
|
@@ -86,10 +86,10 @@ function _createInputDescriptor (input, inputOptions, containerOptions) {
|
|
|
86
86
|
inputDescriptor.limitInputPixels = inputOptions.limitInputPixels
|
|
87
87
|
? Math.pow(0x3FFF, 2)
|
|
88
88
|
: 0;
|
|
89
|
-
} else if (is.integer(inputOptions.limitInputPixels) && inputOptions.limitInputPixels
|
|
89
|
+
} else if (is.integer(inputOptions.limitInputPixels) && is.inRange(inputOptions.limitInputPixels, 0, Number.MAX_SAFE_INTEGER)) {
|
|
90
90
|
inputDescriptor.limitInputPixels = inputOptions.limitInputPixels;
|
|
91
91
|
} else {
|
|
92
|
-
throw is.invalidParameterError('limitInputPixels', 'integer
|
|
92
|
+
throw is.invalidParameterError('limitInputPixels', 'positive integer', inputOptions.limitInputPixels);
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
// unlimited
|
|
@@ -309,9 +309,9 @@ function _isStreamInput () {
|
|
|
309
309
|
* - `size`: Total size of image in bytes, for Stream and Buffer input only
|
|
310
310
|
* - `width`: Number of pixels wide (EXIF orientation is not taken into consideration, see example below)
|
|
311
311
|
* - `height`: Number of pixels high (EXIF orientation is not taken into consideration, see example below)
|
|
312
|
-
* - `space`: Name of colour space interpretation e.g. `srgb`, `rgb`, `cmyk`, `lab`, `b-w` [...](https://libvips.
|
|
312
|
+
* - `space`: Name of colour space interpretation e.g. `srgb`, `rgb`, `cmyk`, `lab`, `b-w` [...](https://www.libvips.org/API/current/VipsImage.html#VipsInterpretation)
|
|
313
313
|
* - `channels`: Number of bands e.g. `3` for sRGB, `4` for CMYK
|
|
314
|
-
* - `depth`: Name of pixel depth format e.g. `uchar`, `char`, `ushort`, `float` [...](https://libvips.
|
|
314
|
+
* - `depth`: Name of pixel depth format e.g. `uchar`, `char`, `ushort`, `float` [...](https://www.libvips.org/API/current/VipsImage.html#VipsBandFormat)
|
|
315
315
|
* - `density`: Number of pixels per inch (DPI), if present
|
|
316
316
|
* - `chromaSubsampling`: String containing JPEG chroma subsampling, `4:2:0` or `4:4:4` for RGB, `4:2:0:4` or `4:4:4:4` for CMYK
|
|
317
317
|
* - `isProgressive`: Boolean indicating whether the image is interlaced using a progressive scan
|
|
@@ -357,7 +357,7 @@ function _isStreamInput () {
|
|
|
357
357
|
* const size = getNormalSize(await sharp(input).metadata());
|
|
358
358
|
*
|
|
359
359
|
* function getNormalSize({ width, height, orientation }) {
|
|
360
|
-
* return orientation || 0 >= 5
|
|
360
|
+
* return (orientation || 0) >= 5
|
|
361
361
|
* ? { width: height, height: width }
|
|
362
362
|
* : { width, height };
|
|
363
363
|
* }
|
package/lib/libvips.js
CHANGED
|
@@ -65,7 +65,13 @@ const isRosetta = function () {
|
|
|
65
65
|
|
|
66
66
|
const globalLibvipsVersion = function () {
|
|
67
67
|
if (process.platform !== 'win32') {
|
|
68
|
-
const globalLibvipsVersion = spawnSync(
|
|
68
|
+
const globalLibvipsVersion = spawnSync('pkg-config --modversion vips-cpp', {
|
|
69
|
+
...spawnSyncOptions,
|
|
70
|
+
env: {
|
|
71
|
+
...env,
|
|
72
|
+
PKG_CONFIG_PATH: pkgConfigPath()
|
|
73
|
+
}
|
|
74
|
+
}).stdout;
|
|
69
75
|
/* istanbul ignore next */
|
|
70
76
|
return (globalLibvipsVersion || '').trim();
|
|
71
77
|
} else {
|
|
@@ -85,7 +91,10 @@ const removeVendoredLibvips = function () {
|
|
|
85
91
|
|
|
86
92
|
const pkgConfigPath = function () {
|
|
87
93
|
if (process.platform !== 'win32') {
|
|
88
|
-
const brewPkgConfigPath = spawnSync(
|
|
94
|
+
const brewPkgConfigPath = spawnSync(
|
|
95
|
+
'which brew >/dev/null 2>&1 && brew environment --plain | grep PKG_CONFIG_LIBDIR | cut -d" " -f2',
|
|
96
|
+
spawnSyncOptions
|
|
97
|
+
).stdout || '';
|
|
89
98
|
return [
|
|
90
99
|
brewPkgConfigPath.trim(),
|
|
91
100
|
env.PKG_CONFIG_PATH,
|
package/lib/output.js
CHANGED
|
@@ -82,6 +82,8 @@ function toFile (fileOut, callback) {
|
|
|
82
82
|
* Write output to a Buffer.
|
|
83
83
|
* JPEG, PNG, WebP, AVIF, TIFF, GIF and raw pixel data output are supported.
|
|
84
84
|
*
|
|
85
|
+
* Use {@link toFormat} or one of the format-specific functions such as {@link jpeg}, {@link png} etc. to set the output format.
|
|
86
|
+
*
|
|
85
87
|
* If no explicit format is set, the output format will match the input image, except SVG input which becomes PNG output.
|
|
86
88
|
*
|
|
87
89
|
* By default all metadata will be removed, which includes EXIF-based orientation.
|
|
@@ -108,6 +110,7 @@ function toFile (fileOut, callback) {
|
|
|
108
110
|
*
|
|
109
111
|
* @example
|
|
110
112
|
* sharp(input)
|
|
113
|
+
* .png()
|
|
111
114
|
* .toBuffer({ resolveWithObject: true })
|
|
112
115
|
* .then(({ data, info }) => { ... })
|
|
113
116
|
* .catch(err => { ... });
|
|
@@ -492,7 +495,7 @@ function webp (options) {
|
|
|
492
495
|
if (is.defined(options.smartSubsample)) {
|
|
493
496
|
this._setBooleanOption('webpSmartSubsample', options.smartSubsample);
|
|
494
497
|
}
|
|
495
|
-
const effort = options.effort
|
|
498
|
+
const effort = is.defined(options.effort) ? options.effort : options.reductionEffort;
|
|
496
499
|
if (is.defined(effort)) {
|
|
497
500
|
if (is.integer(effort) && is.inRange(effort, 0, 6)) {
|
|
498
501
|
this.options.webpEffort = effort;
|
|
@@ -516,7 +519,7 @@ function webp (options) {
|
|
|
516
519
|
* // Convert PNG to GIF
|
|
517
520
|
* await sharp(pngBuffer)
|
|
518
521
|
* .gif()
|
|
519
|
-
* .toBuffer()
|
|
522
|
+
* .toBuffer();
|
|
520
523
|
*
|
|
521
524
|
* @example
|
|
522
525
|
* // Convert animated WebP to animated GIF
|
package/lib/platform.js
CHANGED
|
@@ -7,10 +7,12 @@ const env = process.env;
|
|
|
7
7
|
module.exports = function () {
|
|
8
8
|
const arch = env.npm_config_arch || process.arch;
|
|
9
9
|
const platform = env.npm_config_platform || process.platform;
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
const libc = process.env.npm_config_libc ||
|
|
11
|
+
/* istanbul ignore next */
|
|
12
|
+
(detectLibc.isNonGlibcLinuxSync() ? detectLibc.familySync() : '');
|
|
13
|
+
const libcId = platform !== 'linux' || libc === detectLibc.GLIBC ? '' : libc;
|
|
12
14
|
|
|
13
|
-
const platformId = [`${platform}${
|
|
15
|
+
const platformId = [`${platform}${libcId}`];
|
|
14
16
|
|
|
15
17
|
if (arch === 'arm') {
|
|
16
18
|
const fallback = process.versions.electron ? '7' : '6';
|
package/lib/resize.js
CHANGED
|
@@ -211,7 +211,7 @@ function isRotationExpected (options) {
|
|
|
211
211
|
* @param {String} [options.height] - alternative means of specifying `height`. If both are present this take priority.
|
|
212
212
|
* @param {String} [options.fit='cover'] - how the image should be resized to fit both provided dimensions, one of `cover`, `contain`, `fill`, `inside` or `outside`.
|
|
213
213
|
* @param {String} [options.position='centre'] - position, gravity or strategy to use when `fit` is `cover` or `contain`.
|
|
214
|
-
* @param {String|Object} [options.background={r: 0, g: 0, b: 0, alpha: 1}] - background colour when
|
|
214
|
+
* @param {String|Object} [options.background={r: 0, g: 0, b: 0, alpha: 1}] - background colour when `fit` is `contain`, parsed by the [color](https://www.npmjs.org/package/color) module, defaults to black without transparency.
|
|
215
215
|
* @param {String} [options.kernel='lanczos3'] - the kernel to use for image reduction.
|
|
216
216
|
* @param {Boolean} [options.withoutEnlargement=false] - do not enlarge if the width *or* height are already less than the specified dimensions, equivalent to GraphicsMagick's `>` geometry option.
|
|
217
217
|
* @param {Boolean} [options.withoutReduction=false] - do not reduce if the width *or* height are already greater than the specified dimensions, equivalent to GraphicsMagick's `<` geometry option.
|
package/lib/sharp.js
CHANGED
|
@@ -12,8 +12,11 @@ try {
|
|
|
12
12
|
help.push('- Update Homebrew: "brew update && brew upgrade vips"');
|
|
13
13
|
} else {
|
|
14
14
|
const [platform, arch] = platformAndArch.split('-');
|
|
15
|
+
if (platform === 'linux' && /Module did not self-register/.test(err.message)) {
|
|
16
|
+
help.push('- Using worker threads? See https://sharp.pixelplumbing.com/install#worker-threads');
|
|
17
|
+
}
|
|
15
18
|
help.push(
|
|
16
|
-
'- Install with
|
|
19
|
+
'- Install with verbose logging and look for errors: "npm install --ignore-scripts=false --foreground-scripts --verbose sharp"',
|
|
17
20
|
`- Install for the current ${platformAndArch} runtime: "npm install --platform=${platform} --arch=${arch} sharp"`
|
|
18
21
|
);
|
|
19
22
|
}
|
package/lib/utility.js
CHANGED
|
@@ -103,7 +103,11 @@ cache(true);
|
|
|
103
103
|
|
|
104
104
|
/**
|
|
105
105
|
* Gets or, when a concurrency is provided, sets
|
|
106
|
-
* the number of threads
|
|
106
|
+
* the maximum number of threads _libvips_ should use to process _each image_.
|
|
107
|
+
* These are from a thread pool managed by glib,
|
|
108
|
+
* which helps avoid the overhead of creating new threads.
|
|
109
|
+
*
|
|
110
|
+
* This method always returns the current concurrency.
|
|
107
111
|
*
|
|
108
112
|
* The default value is the number of CPU cores,
|
|
109
113
|
* except when using glibc-based Linux without jemalloc,
|
|
@@ -111,10 +115,19 @@ cache(true);
|
|
|
111
115
|
*
|
|
112
116
|
* A value of `0` will reset this to the number of CPU cores.
|
|
113
117
|
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
118
|
+
* Some image format libraries spawn additional threads,
|
|
119
|
+
* e.g. libaom manages its own 4 threads when encoding AVIF images,
|
|
120
|
+
* and these are independent of the value set here.
|
|
116
121
|
*
|
|
117
|
-
*
|
|
122
|
+
* The maximum number of images that sharp can process in parallel
|
|
123
|
+
* is controlled by libuv's `UV_THREADPOOL_SIZE` environment variable,
|
|
124
|
+
* which defaults to 4.
|
|
125
|
+
*
|
|
126
|
+
* https://nodejs.org/api/cli.html#uv_threadpool_sizesize
|
|
127
|
+
*
|
|
128
|
+
* For example, by default, a machine with 8 CPU cores will process
|
|
129
|
+
* 4 images in parallel and use up to 8 threads per image,
|
|
130
|
+
* so there will be up to 32 concurrent threads.
|
|
118
131
|
*
|
|
119
132
|
* @example
|
|
120
133
|
* const threads = sharp.concurrency(); // 4
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sharp",
|
|
3
3
|
"description": "High performance Node.js image processing, the fastest module to resize JPEG, PNG, WebP, GIF, AVIF and TIFF images",
|
|
4
|
-
"version": "0.30.
|
|
4
|
+
"version": "0.30.7",
|
|
5
5
|
"author": "Lovell Fuller <npm@lovell.info>",
|
|
6
6
|
"homepage": "https://github.com/lovell/sharp",
|
|
7
7
|
"contributors": [
|
|
@@ -82,7 +82,8 @@
|
|
|
82
82
|
"Joris Dugué <zaruike10@gmail.com>",
|
|
83
83
|
"Chris Banks <christopher.bradley.banks@gmail.com>",
|
|
84
84
|
"Ompal Singh <ompal.hitm09@gmail.com>",
|
|
85
|
-
"Brodan <christopher.hranj@gmail.com"
|
|
85
|
+
"Brodan <christopher.hranj@gmail.com",
|
|
86
|
+
"Ankur Parihar <ankur.github@gmail.com>"
|
|
86
87
|
],
|
|
87
88
|
"scripts": {
|
|
88
89
|
"install": "(node install/libvips && node install/dll-copy && prebuild-install) || (node install/can-compile && node-gyp rebuild && node install/dll-copy)",
|
|
@@ -129,22 +130,22 @@
|
|
|
129
130
|
"dependencies": {
|
|
130
131
|
"color": "^4.2.3",
|
|
131
132
|
"detect-libc": "^2.0.1",
|
|
132
|
-
"node-addon-api": "^
|
|
133
|
-
"prebuild-install": "^7.
|
|
133
|
+
"node-addon-api": "^5.0.0",
|
|
134
|
+
"prebuild-install": "^7.1.1",
|
|
134
135
|
"semver": "^7.3.7",
|
|
135
136
|
"simple-get": "^4.0.1",
|
|
136
137
|
"tar-fs": "^2.1.1",
|
|
137
138
|
"tunnel-agent": "^0.6.0"
|
|
138
139
|
},
|
|
139
140
|
"devDependencies": {
|
|
140
|
-
"async": "^3.2.
|
|
141
|
+
"async": "^3.2.4",
|
|
141
142
|
"cc": "^3.0.1",
|
|
142
143
|
"decompress-zip": "^0.3.3",
|
|
143
144
|
"documentation": "^13.2.5",
|
|
144
145
|
"exif-reader": "^1.0.3",
|
|
145
146
|
"icc": "^2.0.0",
|
|
146
147
|
"license-checker": "^25.0.1",
|
|
147
|
-
"mocha": "^
|
|
148
|
+
"mocha": "^10.0.0",
|
|
148
149
|
"mock-fs": "^5.1.2",
|
|
149
150
|
"nyc": "^15.1.0",
|
|
150
151
|
"prebuild": "^11.0.3",
|
package/src/common.cc
CHANGED
|
@@ -48,6 +48,9 @@ namespace sharp {
|
|
|
48
48
|
int32_t AttrAsInt32(Napi::Object obj, unsigned int const attr) {
|
|
49
49
|
return obj.Get(attr).As<Napi::Number>().Int32Value();
|
|
50
50
|
}
|
|
51
|
+
int64_t AttrAsInt64(Napi::Object obj, std::string attr) {
|
|
52
|
+
return obj.Get(attr).As<Napi::Number>().Int64Value();
|
|
53
|
+
}
|
|
51
54
|
double AttrAsDouble(Napi::Object obj, std::string attr) {
|
|
52
55
|
return obj.Get(attr).As<Napi::Number>().DoubleValue();
|
|
53
56
|
}
|
|
@@ -131,7 +134,7 @@ namespace sharp {
|
|
|
131
134
|
}
|
|
132
135
|
}
|
|
133
136
|
// Limit input images to a given number of pixels, where pixels = width * height
|
|
134
|
-
descriptor->limitInputPixels =
|
|
137
|
+
descriptor->limitInputPixels = static_cast<uint64_t>(AttrAsInt64(input, "limitInputPixels"));
|
|
135
138
|
// Allow switch from random to sequential access
|
|
136
139
|
descriptor->access = AttrAsBool(input, "sequentialRead") ? VIPS_ACCESS_SEQUENTIAL : VIPS_ACCESS_RANDOM;
|
|
137
140
|
// Remove safety features and allow unlimited SVG/PNG input
|
|
@@ -439,7 +442,7 @@ namespace sharp {
|
|
|
439
442
|
}
|
|
440
443
|
// Limit input images to a given number of pixels, where pixels = width * height
|
|
441
444
|
if (descriptor->limitInputPixels > 0 &&
|
|
442
|
-
static_cast<uint64_t>(image.width() * image.height()) >
|
|
445
|
+
static_cast<uint64_t>(image.width() * image.height()) > descriptor->limitInputPixels) {
|
|
443
446
|
throw vips::VError("Input image exceeds pixel limit");
|
|
444
447
|
}
|
|
445
448
|
return std::make_tuple(image, imageType);
|
package/src/common.h
CHANGED
package/src/pipeline.cc
CHANGED
|
@@ -95,16 +95,18 @@ class PipelineWorker : public Napi::AsyncWorker {
|
|
|
95
95
|
if (baton->rotateBeforePreExtract) {
|
|
96
96
|
if (rotation != VIPS_ANGLE_D0) {
|
|
97
97
|
image = image.rot(rotation);
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
98
|
+
}
|
|
99
|
+
if (flip) {
|
|
100
|
+
image = image.flip(VIPS_DIRECTION_VERTICAL);
|
|
101
|
+
}
|
|
102
|
+
if (flop) {
|
|
103
|
+
image = image.flip(VIPS_DIRECTION_HORIZONTAL);
|
|
104
|
+
}
|
|
105
|
+
if (rotation != VIPS_ANGLE_D0 || flip || flop) {
|
|
106
106
|
image = sharp::RemoveExifOrientation(image);
|
|
107
107
|
}
|
|
108
|
+
flop = FALSE;
|
|
109
|
+
flip = FALSE;
|
|
108
110
|
if (baton->rotationAngle != 0.0) {
|
|
109
111
|
MultiPageUnsupported(nPages, "Rotate");
|
|
110
112
|
std::vector<double> background;
|
|
@@ -186,8 +188,10 @@ class PipelineWorker : public Napi::AsyncWorker {
|
|
|
186
188
|
if (jpegShrinkOnLoad > 1 && static_cast<int>(shrink) == jpegShrinkOnLoad) {
|
|
187
189
|
jpegShrinkOnLoad /= 2;
|
|
188
190
|
}
|
|
189
|
-
} else if (inputImageType == sharp::ImageType::WEBP
|
|
190
|
-
|
|
191
|
+
} else if (inputImageType == sharp::ImageType::WEBP && shrink > 1.0) {
|
|
192
|
+
// Avoid upscaling via webp
|
|
193
|
+
scale = 1.0 / shrink;
|
|
194
|
+
} else if (inputImageType == sharp::ImageType::SVG ||
|
|
191
195
|
inputImageType == sharp::ImageType::PDF) {
|
|
192
196
|
scale = 1.0 / shrink;
|
|
193
197
|
}
|
|
@@ -605,14 +609,14 @@ class PipelineWorker : public Napi::AsyncWorker {
|
|
|
605
609
|
int across = 0;
|
|
606
610
|
int down = 0;
|
|
607
611
|
// Use gravity in overlay
|
|
608
|
-
if (compositeImage.width() <=
|
|
612
|
+
if (compositeImage.width() <= image.width()) {
|
|
609
613
|
across = static_cast<int>(ceil(static_cast<double>(image.width()) / compositeImage.width()));
|
|
610
614
|
// Ensure odd number of tiles across when gravity is centre, north or south
|
|
611
615
|
if (composite->gravity == 0 || composite->gravity == 1 || composite->gravity == 3) {
|
|
612
616
|
across |= 1;
|
|
613
617
|
}
|
|
614
618
|
}
|
|
615
|
-
if (compositeImage.height() <=
|
|
619
|
+
if (compositeImage.height() <= image.height()) {
|
|
616
620
|
down = static_cast<int>(ceil(static_cast<double>(image.height()) / compositeImage.height()));
|
|
617
621
|
// Ensure odd number of tiles down when gravity is centre, east or west
|
|
618
622
|
if (composite->gravity == 0 || composite->gravity == 2 || composite->gravity == 4) {
|