Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions packages/cogify/src/cogify/cli/__test__/cli.cover.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ describe('cli.cover', () => {
target: new URL('memory://target/'),
preset: 'webp',
tileMatrix: 'WebMercatorQuad',

cutline: undefined,
cutlineBlend: 20,
baseZoomOffset: undefined,
verbose: false,
extraVerbose: false,
requireStacCollection: false,
background: undefined,
};

it('should generate a covering', async () => {
Expand All @@ -50,7 +50,6 @@ describe('cli.cover', () => {
paths: [new URL('memory://source/')],
target: new URL('memory://target/'),
preset: 'webp',

requireStacCollection: true,
tileMatrix: 'WebMercatorQuad',
}).catch((e) => String(e));
Expand Down
26 changes: 21 additions & 5 deletions packages/cogify/src/cogify/cli/cli.cog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { CutlineOptimizer } from '../../cutline.js';
import { SourceDownloader } from '../../download.js';
import { HashTransform } from '../../hash.stream.js';
import { getLogger, logArguments } from '../../log.js';
import { gdalBuildCog, gdalBuildVrt, gdalBuildVrtWarp } from '../gdal.command.js';
import { gdalBuildCog, gdalBuildVrt, gdalBuildVrtWarp, gdalCreate } from '../gdal.command.js';
import { GdalRunner } from '../gdal.runner.js';
import { Url, UrlArrayJsonFile } from '../parsers.js';
import { CogifyCreationOptions, CogifyStacItem, getCutline, getSources } from '../stac.js';
Expand Down Expand Up @@ -87,7 +87,6 @@ export const BasemapsCogifyCreateCommand = command({
const logger = getLogger(this, args);

if (args.docker) process.env['GDAL_DOCKER'] = '1';

const paths = args.fromFile != null ? args.path.concat(args.fromFile) : args.path;

const toCreate = await Promise.all(paths.map(async (p) => loadItem(p, logger)));
Expand Down Expand Up @@ -332,9 +331,26 @@ async function createCog(ctx: CogCreationContext): Promise<URL> {
);
await new GdalRunner(vrtWarpCommand).run(logger);

logger?.debug({ tileId }, 'Cog:Create:Tiff');
// Create the COG from the warped vrt
const cogCreateCommand = gdalBuildCog(new URL(`${tileId}.tiff`, ctx.tempFolder), vrtWarpCommand.output, options);
if (options.background == null) {
// Create the COG from the warped vrt without a forced background
const cogCreateCommand = gdalBuildCog(new URL(`${tileId}.tiff`, ctx.tempFolder), vrtWarpCommand.output, options);
await new GdalRunner(cogCreateCommand).run(logger);
return cogCreateCommand.output;
}

// Create a colored background tiff to fill the empty space in the target cog
const gdalCreateCommand = gdalCreate(new URL(`${tileId}-bg.tiff`, ctx.tempFolder), options.background, options);
await new GdalRunner(gdalCreateCommand).run(logger);

// Create a vrt with the background tiff behind the source file vrt
const vrtMergeCommand = gdalBuildVrt(new URL(`${tileId}-merged.vrt`, ctx.tempFolder), [
gdalCreateCommand.output,
vrtWarpCommand.output,
]);
await new GdalRunner(vrtMergeCommand).run(logger);

// Create the COG from the merged vrt with a forced background
const cogCreateCommand = gdalBuildCog(new URL(`${tileId}.tiff`, ctx.tempFolder), vrtMergeCommand.output, options);
await new GdalRunner(cogCreateCommand).run(logger);
return cogCreateCommand.output;
}
Expand Down
8 changes: 7 additions & 1 deletion packages/cogify/src/cogify/cli/cli.cover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { CutlineOptimizer } from '../../cutline.js';
import { getLogger, logArguments } from '../../log.js';
import { Presets } from '../../preset.js';
import { createTileCover, TileCoverContext } from '../../tile.cover.js';
import { Url, UrlFolder } from '../parsers.js';
import { RgbaType, Url, UrlFolder } from '../parsers.js';
import { createFileStats } from '../stac.js';

const SupportedTileMatrix = [GoogleTms, Nztm2000QuadTms];
Expand Down Expand Up @@ -62,6 +62,11 @@ export const BasemapsCogifyCoverCommand = command({
defaultValue: () => false,
defaultValueIsSerializable: true,
}),
background: option({
type: optional(RgbaType),
long: 'background',
description: 'Replace all transparent COG pixels with this RGBA hexstring color',
}),
},
async handler(args) {
const metrics = new Metrics();
Expand Down Expand Up @@ -95,6 +100,7 @@ export const BasemapsCogifyCoverCommand = command({
metrics,
cutline,
preset: args.preset,
background: args.background,
targetZoomOffset: args.baseZoomOffset,
};

Expand Down
45 changes: 45 additions & 0 deletions packages/cogify/src/cogify/gdal.command.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { Rgba } from '@basemaps/config';
import { Epsg, EpsgCode, TileMatrixSets } from '@basemaps/geo';
import { urlToString } from '@basemaps/shared';

import { Presets } from '../preset.js';
import { GdalCommand } from './gdal.runner.js';
import { CogifyCreationOptions } from './stac.js';

const isPowerOfTwo = (x: number): boolean => (x & (x - 1)) === 0;

export function gdalBuildVrt(targetVrt: URL, source: URL[]): GdalCommand {
if (source.length === 0) throw new Error('No source files given for :' + targetVrt.href);
return {
Expand Down Expand Up @@ -97,3 +100,45 @@ export function gdalBuildCog(targetTiff: URL, sourceVrt: URL, opt: CogifyCreatio
.map(String),
};
}

/**
* Creates an empty tiff where all pixel values are set to the given color.
* Used to force a background so that there are no empty pixels in the final COG.
*
* @param targetTiff the file path and name for the created tiff
* @param color the color to set all pixel values
* @param opt a CogifyCreationOptions object
*
* @returns a 'gdal_create' GdalCommand object
*/
export function gdalCreate(targetTiff: URL, color: Rgba, opt: CogifyCreationOptions): GdalCommand {
const cfg = { ...Presets[opt.preset], ...opt };

const tileMatrix = TileMatrixSets.find(cfg.tileMatrix);
if (tileMatrix == null) throw new Error('Unable to find tileMatrix: ' + cfg.tileMatrix);

const bounds = tileMatrix.tileToSourceBounds(cfg.tile);
const pixelScale = tileMatrix.pixelScale(cfg.zoomLevel);
const size = Math.round(bounds.width / pixelScale);

// if the value of 'size' is not a power of 2
if (!isPowerOfTwo(size)) throw new Error('Size did not compute to a power of 2');

return {
command: 'gdal_create',
output: targetTiff,
args: [
['-of', 'GTiff'],
['-outsize', size, size], // set the size to match that of the final COG
['-bands', '4'],
['-burn', `${color.r} ${color.g} ${color.b} ${color.alpha}`], // set all pixel values to the given color
['-a_srs', tileMatrix.projection.toEpsgString()],
['-a_ullr', bounds.x, bounds.bottom, bounds.right, bounds.y],
['-co', 'COMPRESS=LZW'],
urlToString(targetTiff),
]
.filter((f) => f != null)
.flat()
.map(String),
};
}
2 changes: 1 addition & 1 deletion packages/cogify/src/cogify/gdal.runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export interface GdalCommand {
/** Output file location */
output: URL;
/** GDAL command to use */
command: 'gdalwarp' | 'gdalbuildvrt' | 'gdal_translate';
command: 'gdal_create' | 'gdalwarp' | 'gdalbuildvrt' | 'gdal_translate';
/** GDAL arguments to use */
args: string[];
}
Expand Down
16 changes: 14 additions & 2 deletions packages/cogify/src/cogify/parsers.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
import { pathToFileURL } from 'node:url';

import { parseRgba, Rgba } from '@basemaps/config';
import { fsa } from '@basemaps/shared';
import { Type } from 'cmd-ts';

/**
* Parse a input parameter as a URL.
* Parse an input RGBA hexstring as an RGBA object.
*
* Throws an error if the RGBA hexstring is invalid.
**/
export const RgbaType: Type<string, Rgba> = {
from(str) {
return Promise.resolve(parseRgba(str));
},
};

/**
* Parse an input parameter as a URL.
*
* If it looks like a file path, it will be converted using `pathToFileURL`.
**/
Expand All @@ -19,7 +31,7 @@ export const Url: Type<string, URL> = {
};

/**
* Parse a input parameter as a URL which represents a folder.
* Parse an input parameter as a URL which represents a folder.
*
* If it looks like a file path, it will be converted using `pathToFileURL`.
* Any search parameters or hash will be removed, and a trailing slash added
Expand Down
5 changes: 5 additions & 0 deletions packages/cogify/src/cogify/stac.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createHash } from 'node:crypto';

import { Rgba } from '@basemaps/config';
import { Tile } from '@basemaps/geo';
import { StacCollection, StacItem, StacLink } from 'stac-ts';

Expand Down Expand Up @@ -56,7 +57,11 @@ export interface CogifyCreationOptions {
* @default 'lanczos'
*/
overviewResampling?: GdalResampling;

/** Color with which to replace all transparent COG pixels */
background?: Rgba;
}

export type GdalResampling = 'nearest' | 'bilinear' | 'cubic' | 'cubicspline' | 'lanczos' | 'average' | 'mode';

export type CogifyStacCollection = StacCollection;
Expand Down
6 changes: 6 additions & 0 deletions packages/cogify/src/tile.cover.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Rgba } from '@basemaps/config';
import { ConfigImageryTiff } from '@basemaps/config-loader';
import { BoundingBox, Bounds, EpsgCode, Projection, ProjectionLoader, TileId, TileMatrixSet } from '@basemaps/geo';
import { fsa, LogType, urlToString } from '@basemaps/shared';
Expand Down Expand Up @@ -32,6 +33,8 @@ export interface TileCoverContext {
logger?: LogType;
/** GDAL configuration preset */
preset: string;
/** Optional color with which to replace all transparent COG pixels */
background?: Rgba;
/**
* Override the base zoom to store the output COGS as
*/
Expand Down Expand Up @@ -180,6 +183,9 @@ export async function createTileCover(ctx: TileCoverContext): Promise<TileCoverR
assets: {},
};

// Add the background color if it exists
if (ctx.background) item.properties['linz_basemaps:options'].background = ctx.background;

// Add the source imagery as a STAC Link
for (const src of source) {
const srcLink: CogifyLinkSource = {
Expand Down
9 changes: 8 additions & 1 deletion packages/config/src/color.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,20 @@ export function parseHex(str: string): number {
return val;
}

export interface Rgba {
r: number;
g: number;
b: number;
alpha: number;
}

/**
* Parse a hexstring into RGBA
*
* Defaults to 0 if missing values
* @param str string to parse
*/
export function parseRgba(str: string): { r: number; g: number; b: number; alpha: number } {
export function parseRgba(str: string): Rgba {
if (str.startsWith('0x')) str = str.slice(2);
else if (str.startsWith('#')) str = str.slice(1);
if (str.length !== 6 && str.length !== 8) {
Expand Down
2 changes: 1 addition & 1 deletion packages/config/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export {
} from './base.config.js';
export { base58, isBase58 } from './base58.js';
export { ensureBase58, sha256base58 } from './base58.node.js';
export { parseHex, parseRgba } from './color.js';
export { parseHex, parseRgba, Rgba } from './color.js';
export { ConfigBase as BaseConfig } from './config/base.js';
export { ConfigBundle } from './config/config.bundle.js';
export { ConfigImagery, ConfigImageryOverview, ImageryBandType, ImageryDataType } from './config/imagery.js';
Expand Down
Loading