{"id":20127295,"url":"https://github.com/ama-team/php-projection-framework","last_synced_at":"2026-05-10T08:50:16.676Z","repository":{"id":56946611,"uuid":"104381100","full_name":"ama-team/php-projection-framework","owner":"ama-team","description":null,"archived":false,"fork":false,"pushed_at":"2018-06-22T16:02:55.000Z","size":113,"stargazers_count":0,"open_issues_count":8,"forks_count":2,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-01-13T08:21:48.090Z","etag":null,"topics":["pano","panorama","projection","projection-mapping"],"latest_commit_sha":null,"homepage":null,"language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ama-team.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2017-09-21T17:59:00.000Z","updated_at":"2017-11-11T06:16:49.000Z","dependencies_parsed_at":"2022-08-21T07:50:08.624Z","dependency_job_id":null,"html_url":"https://github.com/ama-team/php-projection-framework","commit_stats":null,"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ama-team%2Fphp-projection-framework","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ama-team%2Fphp-projection-framework/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ama-team%2Fphp-projection-framework/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ama-team%2Fphp-projection-framework/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ama-team","download_url":"https://codeload.github.com/ama-team/php-projection-framework/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":241571656,"owners_count":19984135,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["pano","panorama","projection","projection-mapping"],"created_at":"2024-11-13T20:20:22.662Z","updated_at":"2026-05-10T08:50:11.626Z","avatar_url":"https://github.com/ama-team.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# AmaTeam\\Image\\Projection\n\n[![Packagist](https://img.shields.io/packagist/v/ama-team/projection-framework.svg?style=flat-square)](https://packagist.org/packages/ama-team/projection-framework)\n[![CircleCI branch](https://img.shields.io/circleci/project/github/ama-team/php-projection-framework/master.svg?style=flat-square)](https://circleci.com/gh/ama-team/php-projection-framework/tree/master)\n[![Coveralls](https://img.shields.io/coveralls/ama-team/php-projection-framework/master.svg?style=flat-square)](https://coveralls.io/github/ama-team/php-projection-framework?branch=master)\n[![Scrutinizer](https://img.shields.io/scrutinizer/g/ama-team/php-projection-framework/master.svg?style=flat-square)](https://scrutinizer-ci.com/g/ama-team/php-projection-framework)\n[![Code Climate](https://img.shields.io/codeclimate/github/ama-team/php-projection-framework.svg?style=flat-square)](https://codeclimate.com/github/ama-team/php-projection-framework)\n\n\u003csup\u003e\u003csub\u003e\nDon't believe the coverage, it's [lying](https://github.com/sebastianbergmann/php-code-coverage/issues/409).\n\u003c/sub\u003e\u003c/sup\u003e\n\nThis is a simple library created for common work with sphere \nprojections - at the moment of writing, to convert equirectangular and\ncube map projection types one into another.\n\n## Installation\n\n```bash\ncomposer require ama-team/image-projection-framework\n```\n\nPHP 5.6+ and GD / Imagick are required. All file operations are done \nthrough `league/flysystem`, which is explicitly set as dependency.\n\n## Usage\n\n### Sixty-second start\n\n```php\nuse \\AmaTeam\\Image\\Projection\\Framework;\nuse \\AmaTeam\\Image\\Projection\\Framework\\EncodingOptions;\nuse \\AmaTeam\\Image\\Projection\\Specification;\nuse \\AmaTeam\\Image\\Projection\\Image\\Format;\n\n$framework = new Framework();\n$source = new Specification('equirect', 'tmp/uploads/source.jpg');\n$target = new Specification(\n    'cube',\n    'static/pano/{f}/{x}/{y}.jpg',\n    new Box(512, 512) // tile size,\n    new Box(2, 2) // layout, amount of tiles horizontally and vertically\n);\n\n$options = (new EncodingOptions())-\u003esetQuality(0.9);\n\n$framework-\u003econvert($source, $target, Format::JPEG, $options);\n```\n\nYou'll need a `Framework` instance to start off, it will use `cwd()` as\nfs root. Next, `Framework` has `#convert()` and `#convertAll()` methods\nto turn one specification into other (others), optionally specifying\nformat and encoding options (don't worry, it's already jpeg / 90% by \ndefault). Specification is basically a description of a pano: it's \ntype, it's location, it's face size and tile size. Location allows \nseveral placeholders - {f} or {face}, {x} and {y} to automatically \nrecognize / populate it with tile parameters. You won't need them at \nall for equirectangular images, while faces for cubemap are named using\ntheir first letters - u(p), f(ront), b(ack), l(eft), r(ight), d(own). \nType is found using case-insensitive prefix search, so \n`EQUIRECTANGULAR` and `equirect` is basically the same.\n\nChances are you will be creating multi-resolution cube map out of \nequirectangular projection, and to lessen resource consumption,\nsource projection should be read just once - this can be easily \ndone using `#convertAll()`, which uses single reader:\n\n```php\n$targets = array_map(function ($index) {\n    $side = 256 * (int) pow($index, 2);\n    $tileSide = min($side, 512);\n    $size = $side / $side;\n    $tileSize = new Box($tileSide, $tileSide);\n    $layout = new Box($size, $size);\n    $path = sprintf('static/pano/%d/{f}/{x}/{y}.jpg', $index);\n    return new Specification('cube', $path, $tileSize, $layout);\n}, range(0, 3));\n\n$framework-\u003econvertAll($source, $targets);\n```\n\nThis should do the trick.\n\nThe last thing to tell: **it is extremely slow**. The current \nprocessing model fills target projection pixel-by-pixel, so it takes\na while (up to minute on my laptop, cheap servers may be quite slower).\nAnd yes, it will burn a single core as much as it can. Also don't \nforget that source projection has to fit in memory, don't blame me for \nyour images being too big.\n\n## Conventions\n\nProjections are represented as a three-level deep tile structures. \nEvery projection consists of arbitrary amount of faces (six for cube \nmap, one for equirectangular and planet), and each face is a \ntwo-dimension grid of tiles. Tile position, which is determined as\n{face, x, y}, is expressed through curly-braces placeholders, as in the\nmain usage example. All projections follow that rule (even though \nequirectangular is unlikely to have multiple tiles), but that may change \nin future releases.\n\nFramework assumes that all tiles have the exact sizes. Not following \nthat rule may result in undefined behavior and errors.\n\nFlysystem usage assumes that all paths are specified using slash\n(not backslash), and there are no absolute paths at all, all paths\nare relative to root.\n\n## Considerations\n\nThis library processes images by creating virtual sphere accessor from\nsource projection and then using it to populate target projection, \ntexel by texel. This raises following problems:\n\n- To complete target projection, source projection has to be fully\nloaded into RAM. This could be quite a lot for big projections, and\nthe library itself doesn't do any size validations, so you have to \nwatch RAM yourself. Also, image loading adds time penalty itself.\n- The main workload is giant loop that asks for color of specific\nsphere coordinates and then populates target image with that color.\nThis results in {number of pixels} function calls, and that number is\nusually huge even for modern processor - generating 2048x2048 cube map\nwill need to populate 24M pixels, and all that work would be done on\nsingle CPU core, and there is not much overhead to optimize.\n**It is slow as hell and not much can be done**. Generating such a\nprojection for two minutes is OK, having one core occupied at 100% is\nexpected as well. All you can do is to parallelize work on several\ncores by filtering which tiles should be generated; this, however,\nwill require to load source projection into RAM for every core.\n\nIf you want to convert projections faster or exploit GPU, you will need\nto implement the same thing yourself, most probably using other \nlanguage. Image processing was always a complex thing and PHP is not \nenough to solve it efficiently.\n\n## Filtering generated tiles, adding processors and listeners\n\nAll the specified above needs more fine work and other methods.\n`Framework` instance provides method `getConverter()` that allows more\ndetailed work using methods `createConversion()` and\n`createConversions()`. Those will create an object ready to \nperform conversion, but not yet launched (which is done through\n`#run()` method), and which expose additional processing capabilities.\n\nIf you've considered parallelizing work by scheduling different tiles\nto different workers, you'll need to restrict some tiles from being \ncreated on particular worker. Filter functionality exists just for \nthis case:\n\n```php\n// only specified faces will be created\n$filter = new FaceFilter('f', 'b');\n$options = (new ConversionOptions())-\u003esetFilters([$filter]);\n$framework\n    -\u003egetConverter()\n    -\u003ecreateConversion($source, $target, $options)\n    -\u003erun();\n```\n\nIf you need to add some kind of watermark or apply custom antialiasing,\nyou can do it via processor which is run on tile after it's generation:\n\n```php\n$fxaaProcessor = new FXAAProcessor();\n$watermarkProcessor = new WatermarkProcessor();\n$conversion = $framework\n    -\u003egetConverter()\n    -\u003ecreateConversion($source, $target);\n    // 50 is order in which processor will run, so it will run before\n    // watermark processor\n    -\u003eaddProcessor(50, $fxaaProcessor)\n    -\u003eaddProcessor(99, $watermarkProcessor);\n    -\u003erun();\n```\n\nLast thing to mention is listeners - those are simply some side-effect\ngenerators that accept fully generated tiles. The most common example\nis SaveListener that is not included in conversion by default:\n\n```php\n$filesystem = $framework-\u003egetRegistry()-\u003egetFilesystem();\n$listener = new SaveListener($filesystem, Format::JPEG);\n$conversion = $framework\n    -\u003egetConverter()\n    -\u003ecreateConversion($source, $target)\n    -\u003eaddListener($listener)\n    -\u003erun();\n```\n\nThis is what actually default methods in sixty second example do.\n\n## Adding custom projection type\n\nAll projections are quite the same: they are bunch of images with some\nrules of mapping sphere on it and vice versa. All you actually need is\nto implement `MappingInterface`, bury it in AbstractHandler child and\nregister in framework:\n\n```php\n$framework = new Framework();\n$framework-\u003eregister('LittlePlanet', new LittlePlanetHandler()); \n``` \n\nYou're ready to go.\n\n## Contributing\n\nFeel free to fork and send PR to **dev** branch.\n\n### Testing\n\nTesting is done using Codeception, and everything, except for directory\nstructure and installable fixtures, is quite the usual.\n\nTo install the fixtures, simply run `test:setup` task:\n\n```bash\nbin/robo test:setup\n```\n\nThis will download some public license test images from flickr \nand put them into `tests/Data/External/Projections`, as well as\ncur them into faces.\n\nYou can run tests by using `test` and `test:\u003csuite\u003e` commands:\n\n```bash\nbin/robo test\nbin/robo test:acceptance\nbin/robo test:unit --coverage\n```\n\nAcceptance tests are doing some real work mangling millions of texels, \nso you should never ever turn on coverage for them unless you are on\nIntel Stress Test team. They are already slow enough, believe me.\n\nFollowing command will generate coverage and [Allure][] reports:\n\n```bash\nbin/robo test:report\n```\n\n### Dev branch state\n\n[![CircleCI](https://img.shields.io/circleci/project/github/ama-team/php-projection-framework/dev.svg?style=flat-square)](https://circleci.com/gh/ama-team/php-projection-framework/tree/dev)\n[![Coveralls](https://img.shields.io/coveralls/ama-team/php-projection-framework/dev.svg?style=flat-square)](https://coveralls.io/github/ama-team/php-projection-framework?branch=dev)\n[![Scrutinizer](https://img.shields.io/scrutinizer/g/ama-team/php-projection-framework/dev.svg?style=flat-square)](https://scrutinizer-ci.com/g/ama-team/php-projection-framework)\n\n## License\n\nMIT License\n\nAMA Team, 2017\n\n  [allure]: https://github.com/allure-framework/allure\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fama-team%2Fphp-projection-framework","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fama-team%2Fphp-projection-framework","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fama-team%2Fphp-projection-framework/lists"}