├── .gitignore ├── COPYING ├── README.md ├── examples ├── pen_plotter.jpg └── test.png ├── rastercarve ├── __init__.py └── __main__.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc. 5 | 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Library General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RasterCarve 2 | 3 | [![PyPI version](https://badge.fury.io/py/rastercarve.svg)](https://badge.fury.io/py/rastercarve) [![PyPI license](https://img.shields.io/pypi/l/rastercarve.svg)](https://pypi.python.org/pypi/rastercarve/) [![PyPI status](https://img.shields.io/pypi/status/rastercarve.svg)](https://pypi.python.org/pypi/rastercarve/) 4 | 5 | This is a little Python script I wrote to generate G-code toolpaths to 6 | engrave raster images. 7 | 8 | A hosted version of the script is available at 9 | https://rastercarve.live 10 | ([Github](https://github.com/built1n/rastercarve-live)). There is also 11 | a standalone custom G-code previewer available at 12 | https://github.com/built1n/rastercarve-preview. 13 | 14 | It takes bitmap images and produces commands (G-code) for a CNC 15 | machine to engrave that image onto a piece of material. For the 16 | uninitiated, a CNC machine is essentially a robotic carving machine -- 17 | think *robot drill*: you 1) put in a piece of wood/foam/aluminum 18 | stock; 2) program the machine; and 3) out comes a finished piece with 19 | the right patterns cut into it. 20 | 21 | This program comes in during step 2 -- it takes an image and outputs 22 | the right sequence of commands for your machine to engrave it. This is 23 | not the first program that can do this, but existing solutions are 24 | unsuitable due to their high cost. 25 | 26 | The program's output has been thoroughly tested on a ShopBot Desktop 27 | MAX, which produced the results shown below, and a ShopBot PRTalpha. 28 | Various users have reported successful results on X-Carve and Shapeoko 29 | machines, among others. 30 | 31 | 32 | 33 | 34 | 35 | # Installation 36 | 37 | `$ pip install rastercarve` 38 | 39 | Running straight from the source tree works fine, too: 40 | 41 | `$ python -m rastercarve -h` 42 | 43 | # Usage 44 | 45 | ``` 46 | $ rastercarve --width 10 examples/test.png > out.nc 47 | Generating G-code: 100%|██████████████████| 278/278 [00:04<00:00, 57.10 lines/s] 48 | === Statistics === 49 | Input resolution: 512x512 px 50 | Output dimensions: 10.00" wide by 10.00" tall = 100.0 in^2 51 | Max line depth: 0.080 in 52 | Max line width: 0.043 in (30.0 deg V-bit) 53 | Line spacing: 0.047 in (110% stepover) 54 | Line angle: 22.5 deg 55 | Number of lines: 277 56 | Input resolution: 51.2 PPI 57 | Output resolution: 100.0 PPI 58 | Scaled image by f=3.91 (200.0 PPI) 59 | Total toolpath length: 2202.6 in 60 | - Rapids: 34.6 in (8.6 s) 61 | - Plunges: 29.8 in (59.6 s) 62 | - Moves: 2138.2 in (1282.9 s) 63 | Feed rate: 100.0 in/min 64 | Plunge rate: 30.0 in/min 65 | Estimated machining time: 1351.2 sec 66 | 1 suppressed debug message(s). 67 | ``` 68 | 69 | This command generates G-code to engrave `examples/test.png` into an 70 | piece of material 10 inches wide. Exactly one of the `--width` or 71 | `--height` parameters must be specified on the command line; the other 72 | will be calculated automatically. 73 | 74 | The engraving parameters can be safely left at their defaults, though 75 | fine-tuning is possible depending on material and machine 76 | characteristics. 77 | 78 | The output G-code will be piped to `out.nc`, which any CNC machine 79 | should accept as input. 80 | 81 | # Machining Process 82 | 83 | With the toolpath generated, it is time to run the job. Presumably you 84 | know the specifics of your particular machine, so I'll only outline 85 | the high-level steps here: 86 | 87 | 1. Load the right tool. An engraving bit is best, though ordinary 88 | V-bits give acceptable results. Make sure that the tool angle matches 89 | that used to generate the toolpath (30 degrees is the default -- 90 | change this if needed). 91 | 92 | 2. Load the material. MDF seems to work best; plywood and ordinary 93 | lumber are too prone to chipping. Plastics have a tendency to melt and 94 | stick to the bit. 95 | 96 | 3. Zero X and Y axes at the top left corner of the eventual image 97 | location. Double check that the bottom right corner is in bounds. 98 | 99 | 4. Zero the Z axis to the top surface of the material. 100 | 101 | 5. Load and run the toolpath. The engraving will begin in the top 102 | right corner and work its way down to the bottom right in a serpentine 103 | fashion. 104 | 105 | ## Ramping 106 | 107 | Some tools (e.g. ShopBot) have an option to control acceleration ramping 108 | speeds. The intricate nature of many raster engraving toolpaths generated 109 | with this program tend to trigger unneccessary speed ramping on these 110 | machines, leading to very slow cycle times. The solution to this is to 111 | set more aggressive ramping values. (ShopBot users can use [VR].) 112 | 113 | # Advanced 114 | 115 | ``` 116 | usage: rastercarve [-h] (--width WIDTH | --height HEIGHT) [-f FEED_RATE] 117 | [-p PLUNGE_RATE] [--rapid RAPID_RATE] [-z SAFE_Z] 118 | [--end-z TRAVERSE_Z] [-t TOOL_ANGLE] [-d MAX_DEPTH] 119 | [-a LINE_ANGLE] [-s STEPOVER] [-r LINEAR_RESOLUTION] 120 | [--dots] [--no-line-numbers] 121 | [--preamble PREAMBLE | --preamble-file PREAMBLE_FILE] 122 | [--epilogue EPILOGUE | --epilogue-file EPILOGUE_FILE] 123 | [--json JSON_DEST] [--debug] [-q] [--version] 124 | filename 125 | 126 | Generate G-code to engrave raster images. 127 | 128 | positional arguments: 129 | filename input image (any OpenCV-supported format) 130 | 131 | optional arguments: 132 | -h, --help show this help message and exit 133 | --json JSON_DEST dump statistics in JSON format 134 | --debug print debug messages 135 | -q disable progress and statistics 136 | --version show program's version number and exit 137 | 138 | output dimensions: 139 | Exactly one required. Image will be scaled while maintaining aspect ratio. 140 | 141 | --width WIDTH output width (in) 142 | --height HEIGHT output height (in) 143 | 144 | machine configuration: 145 | -f FEED_RATE engraving feed rate (in/min) (default: 100) 146 | -p PLUNGE_RATE engraving plunge rate (in/min) (default: 30) 147 | --rapid RAPID_RATE rapid traverse rate (for time estimation only) 148 | (default: 240) 149 | -z SAFE_Z rapid traverse height (in) (default: 0.1) 150 | --end-z TRAVERSE_Z Z height of final traverse (in) (default: 2) 151 | -t TOOL_ANGLE included angle of tool (deg) (default: 30) 152 | 153 | engraving parameters: 154 | -d MAX_DEPTH maximum engraving depth (in) (default: 0.08) 155 | -a LINE_ANGLE angle of grooves from horizontal (deg) (default: 22.5) 156 | -s STEPOVER stepover percentage (affects spacing between lines) 157 | (default: 110) 158 | -r LINEAR_RESOLUTION distance between successive G-code points (in) 159 | (default: 0.01) 160 | --dots engrave using dots instead of lines (experimental) 161 | 162 | G-code parameters: 163 | --no-line-numbers suppress G-code line numbers (dangerous on ShopBot!) 164 | --preamble PREAMBLE override the default G-code preamble; to specify 165 | multiple lines on the command line, use $'' strings 166 | with \n; each line of the preamble will be prepended 167 | with a line number, except when used with --no-line- 168 | numbers 169 | --preamble-file PREAMBLE_FILE 170 | like --preamble, but read from a file 171 | --epilogue EPILOGUE override the default G-code epilogue; see above notes 172 | for --preamble 173 | --epilogue-file EPILOGUE_FILE 174 | like --epilogue, but read from a file 175 | 176 | The default feeds have been found to be safe values for medium-density 177 | fiberboard (MDF). Experimenting with the STEPOVER, LINE_ANGLE, and 178 | LINEAR_RESOLUTION may yield improvements in engraving quality at the cost of 179 | increased machining time. On ShopBot machines, the --no-line-numbers flag must 180 | not be used, since the spindle will fail to start and damage the material. Use 181 | this flag with caution on other machines. 182 | ``` 183 | 184 | ## G-code Customization 185 | 186 | The G-code produced should work out-of-the-box on ShopBot machines. Other 187 | machines may need some fine-tuning. 188 | 189 | ### Preamble 190 | 191 | The default G-code preamble is 192 | 193 | ``` 194 | G00 G20 195 | M03 196 | ``` 197 | 198 | This tells the machine to use inch units (`G20`) and then starts the 199 | spindle (`M03`). 200 | 201 | The default G-code epilogue is 202 | 203 | ``` 204 | M05 205 | ``` 206 | 207 | This does nothing but stop the spindle. 208 | 209 | The `--preamble[-file]` and `--epilogue[-file]` options allow you to specify 210 | a custom G-code header or footer to override the default. Note that in writing 211 | a custom preamble/epilogue, you should *not* include line numbers; the program 212 | will automatically insert them on each line of the supplied preamble/epilogue. 213 | 214 | ## Metric Units 215 | 216 | Passing the `--metric` flag will replace the default `G20` directive with `G21` 217 | to force metric units. If this is passed, all measurements given will be 218 | interpreted as millimeters. E.g., `--width 100` will be interpreted as a width 219 | of 100mm. (That is to say, the `--metric` flag is comparatively dumb; no 220 | internal unit scaling takes place -- only the preamble is changed.) 221 | 222 | Note that the `--metric` flag cannot be used in conjunction with 223 | `--preamble[-file]`. If a custom preamble is necessary with metric 224 | units, just include `G21` in the custom preamble. 225 | 226 | ## Pen Plotting 227 | 228 | The generated toolpaths produce excellent results when used with pen plotters 229 | instead of engraving bits (see above). The machine setup is a little more 230 | complicated, though: the Z height must be set to half the maximum engraving 231 | depth *above* the material for the black and white regions to be drawn 232 | correctly. 233 | 234 | # Related 235 | 236 | [Vectric PhotoVCarve](https://www.vectric.com/products/photovcarve) - 237 | a similar commercial solution. This program is not derived from 238 | PhotoVCarve. 239 | 240 | [My blog post](https://www.fwei.tk/blog/opening-black-boxes.html) - 241 | writeup on the development process. 242 | -------------------------------------------------------------------------------- /examples/pen_plotter.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/built1n/rastercarve/e614cf1072363c37a6b678af9ac6dc5677f9919a/examples/pen_plotter.jpg -------------------------------------------------------------------------------- /examples/test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/built1n/rastercarve/e614cf1072363c37a6b678af9ac6dc5677f9919a/examples/test.png -------------------------------------------------------------------------------- /rastercarve/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # RasterCarve 5 | # 6 | # Copyright (C) 2019-2020 Franklin Wei 7 | # 8 | # This program is free software; you can redistribute it and/or 9 | # modify it under the terms of the GNU General Public License 10 | # as published by the Free Software Foundation; either version 2 11 | # of the License, or (at your option) any later version. 12 | # 13 | # This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY 14 | # KIND, either express or implied. 15 | 16 | __version__ = '1.0.8' 17 | -------------------------------------------------------------------------------- /rastercarve/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # RasterCarve 5 | # 6 | # Copyright (C) 2019-2020 Franklin Wei 7 | # 8 | # This program is free software; you can redistribute it and/or 9 | # modify it under the terms of the GNU General Public License 10 | # as published by the Free Software Foundation; either version 2 11 | # of the License, or (at your option) any later version. 12 | # 13 | # This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY 14 | # KIND, either express or implied. 15 | 16 | import math 17 | import sys 18 | 19 | from rastercarve import __version__ 20 | 21 | import argparse 22 | import cv2 # image scaling 23 | import io # stringIO for parsing lines 24 | import json 25 | import numpy as np # a little vector stuff 26 | from tqdm import tqdm # progress bar 27 | 28 | glob_args = None 29 | 30 | ##### Default parameters 31 | #### Machine configuration 32 | DEF_FEED_RATE = 100 # in / min 33 | DEF_PLUNGE_RATE = 30 # in / min 34 | DEF_RAPID_RATE = 240 # in / min (used only for time estimation) 35 | DEF_SAFE_Z = .1 # tool will start/end this high from material 36 | DEF_TRAVERSE_Z = 2 # ending height (in) 37 | DEF_MAX_DEPTH = .080 # full black is this many inches deep 38 | DEF_TOOL_ANGLE = 30 # included angle of tool (we assume a V-bit). change if needed 39 | 40 | #### Cutting parameters 41 | DEF_STEPOVER = 110 42 | DEF_LINE_ANGLE = 22.5 # angle of lines across image, [0-90) degrees 43 | DEF_LINEAR_RESOLUTION = .01 # spacing between image samples along a line (inches) 44 | 45 | #### G-code parameters 46 | DEF_PREAMBLE = "G00 G20\nM03" 47 | DEF_PREAMBLE_METRIC = "G00 G21\nM03" 48 | DEF_EPILOGUE = "M05" 49 | 50 | #### Image interpolation 51 | SUPERSAMPLE = 2 # scale heightmap by this factor before cutting 52 | 53 | #### Internal stuff - don't mess with this 54 | DEG2RAD = math.pi / 180 55 | 56 | # Unit names 57 | UNIT_LONG = "in" 58 | UNIT_SHORT = "\"" 59 | MM_LONG = MM_SHORT = "mm" 60 | 61 | #### Parameter constraints 62 | CONSTRAINTS = [ 63 | '0 <= glob_args.line_angle < 90', 64 | '0 < glob_args.tool_angle < 180', 65 | '0 < glob_args.feed_rate', 66 | '0 < glob_args.plunge_rate', 67 | '0 < glob_args.safe_z', 68 | '0 < glob_args.traverse_z', 69 | '0 < glob_args.max_depth', 70 | '100 <= glob_args.stepover', 71 | '0 < glob_args.linear_resolution', 72 | '(hasattr(glob_args, "height") and glob_args.height > 0) or (hasattr(glob_args, "width") and glob_args.width > 0)', 73 | 'not (hasattr(glob_args, "metric") and (hasattr(glob_args, "preamble") or hasattr(glob_args, "preamble_file")))' 74 | ] 75 | 76 | # floating-point range 77 | def frange(x, y, jump): 78 | while x < y: 79 | yield x 80 | x += jump 81 | 82 | def eprint(s): 83 | if not hasattr(glob_args, 'quiet'): 84 | print(s, file=sys.stderr) 85 | 86 | debug_msgs = 0 87 | def debug(str): 88 | global debug_msgs 89 | if hasattr(glob_args, 'debug'): 90 | eprint(str) 91 | debug_msgs += 1 92 | 93 | line_no = 1 94 | def gcode(s): 95 | global line_no 96 | print((s if hasattr(glob_args, 'suppress_linenos') else "N%d %s" % (line_no, s)) ) 97 | line_no += 1 98 | 99 | def gcode_multiline(s): 100 | f = io.StringIO(s) 101 | for l in f: 102 | gcode(l.rstrip()) 103 | 104 | 105 | pathlen = 0 # in 106 | rapidlen = 0 # in 107 | plungelen = 0 # in 108 | movelen = 0 # in 109 | pathtime = 0 # sec 110 | lastpos = None 111 | 112 | # movetype: 1 = feed, 2 = rapid, 3 = plunge 113 | def updatePos(pos, feedrate, movetype): 114 | global pathlen, rapidlen, plungelen, movelen, lastpos, pathtime 115 | if lastpos is None: 116 | lastpos = pos 117 | return 118 | d = np.linalg.norm(pos - lastpos) 119 | pathlen += d 120 | 121 | # account for different types of moves separately 122 | if movetype == 1: 123 | movelen += d 124 | elif movetype == 2: 125 | rapidlen += d 126 | elif movetype == 3: 127 | plungelen += d 128 | 129 | pathtime += d / (feedrate / 60) 130 | lastpos = pos 131 | 132 | # reflect as needed 133 | def transform(x, y): 134 | return x, -y 135 | 136 | # we will negate the Y axis in all these 137 | def move(x, y, z, f): 138 | x, y = transform(x, y) 139 | gcode("G1 F%d X%f Y%f Z%f" % (f, x, y, z)) 140 | updatePos(np.array([x, y, z]), f, 1) 141 | 142 | def moveRapid(x, y, z): 143 | x, y = transform(x, y) 144 | gcode("G0 X%f Y%f Z%f" % (x, y, z)) 145 | updatePos(np.array([x, y, z]), glob_args.rapid_rate, 2) 146 | 147 | def moveSlow(x, y, z): 148 | x, y = transform(x, y) 149 | f = glob_args.plunge_rate 150 | gcode("G1 F%d X%f Y%f Z%f" % (f, x, y, z)) 151 | updatePos(np.array([x, y, z]), f, 3) 152 | 153 | def moveRapidXY(x, y): 154 | x, y = transform(x, y) 155 | gcode("G0 X%f Y%f" % (x, y)) 156 | updatePos(np.array([x, y, lastpos[2]]), glob_args.rapid_rate, 2) 157 | 158 | def moveZ(z, f): 159 | gcode("G1 F%d Z%f" % (f, z)) 160 | newpos = lastpos 161 | newpos[2] = z 162 | updatePos(newpos, f, 2) 163 | 164 | def getPix(image, x, y): 165 | # clamp 166 | x = max(0, min(int(x), image.shape[1]-1)) 167 | y = max(0, min(int(y), image.shape[0]-1)) 168 | 169 | return image[y, x] 170 | 171 | # return how deep to cut given a pixel value 172 | def getDepth(pix): 173 | # may want to do gamma mapping 174 | return -float(pix) / 256 * glob_args.max_depth 175 | 176 | def inBounds(img_size, x): 177 | return 0 <= x[0] and x[0] <= img_size[0] and 0 <= x[1] and x[1] <= img_size[1] 178 | 179 | # Engrave one line across the image. start and d are vectors in the 180 | # output space representing the start point and direction of 181 | # machining, respectively. start should be on the border of the image, 182 | # and d should point INTO the image. 183 | def engraveLine(img_interp, img_size, ppi, start, d, step): 184 | v = start 185 | d = d / np.linalg.norm(d) 186 | 187 | # disabled -FW 188 | # if not inBounds(img_size, v): 189 | # debug("Refusing to engrave out of bounds. (Possible programming error, you idiot!): %s, %s" % (v, img_size)) 190 | # return start 191 | 192 | moveZ(glob_args.safe_z, glob_args.plunge_rate) 193 | moveRapidXY(v[0], v[1]) 194 | 195 | first = True 196 | quitNext = False 197 | while True: 198 | img_x = int(round(v[0] * ppi)) 199 | img_y = int(round(v[1] * ppi)) 200 | x, y = v 201 | depth = getDepth(getPix(img_interp, img_x, img_y)) 202 | if not first: 203 | if hasattr(glob_args, 'pointmode'): 204 | move(x, y, glob_args.safe_z, glob_args.feed_rate) 205 | move(x, y, depth, glob_args.feed_rate) 206 | move(x, y, glob_args.safe_z, glob_args.feed_rate) 207 | else: 208 | move(x, y, depth, glob_args.feed_rate) 209 | else: 210 | first = False 211 | moveSlow(x, y, depth) 212 | if hasattr(glob_args, 'pointmode'): 213 | moveSlow(x, y, glob_args.safe_z) 214 | 215 | if quitNext: 216 | return v 217 | v += step * d 218 | 219 | # engrave to the edge 220 | if not inBounds(img_size, v): 221 | v -= step * d 222 | c = ((img_size[0] - v[0]) / d[0]) if (d[0] > 0 and d[1] == 0) else \ 223 | min(-v[1] / d[1], (img_size[0] - v[0]) / d[0]) if (d[0] > 0 and d[1] != 0) else \ 224 | min(-v[0] / d[0], (img_size[1] - v[1]) / d[1]) 225 | v += c * d 226 | 227 | quitNext = True 228 | 229 | # return last engraved point 230 | return v 231 | 232 | def checkCondition(cond): 233 | success = eval(cond) 234 | if not success: 235 | eprint("ERROR: Invalid parameter: %s" % cond) 236 | return success 237 | 238 | def dump_stats(orig_size, img_size, line_width, line_spacing, 239 | nlines, img_ppi, output_ppi, scale_factor, interp_ppi, pathlen, 240 | rapidlen, plungelen, movelen, pathtime): 241 | eprint("=== Statistics ===") 242 | eprint(f"Input resolution: %dx%d px" % (orig_size[0], orig_size[1])) 243 | eprint(f"Output dimensions: %.2f{UNIT_SHORT} wide by %.2f{UNIT_SHORT} tall = %.1f {UNIT_LONG}^2" % (img_size[0], img_size[1], img_size[0] * img_size[1])) 244 | eprint(f"Max line depth: %.3f {UNIT_LONG}" % (glob_args.max_depth)) 245 | eprint(f"Max line width: %.3f {UNIT_LONG} (%.1f deg V-bit)" % (line_width, glob_args.tool_angle)) 246 | eprint(f"Line spacing: %.3f {UNIT_LONG} (%d%% stepover)" % (line_spacing, int(round(glob_args.stepover)))) 247 | eprint(f"Line angle: %.1f deg" % (glob_args.line_angle)) 248 | eprint(f"Number of lines: %d" % (nlines)) 249 | eprint(f"Input resolution: %.1f PPI" % (img_ppi)) 250 | eprint(f"Output resolution: %.1f PPI" % (output_ppi)) 251 | eprint(f"Scaled image by f=%.2f (%.1f PPI)" % (scale_factor, interp_ppi)) 252 | eprint(f"Total toolpath length: %.1f {UNIT_LONG}" % (pathlen)) 253 | eprint(f" - Rapids: %.1f {UNIT_LONG} (%.1f sec)" % (rapidlen, rapidlen / (glob_args.rapid_rate / 60))) 254 | eprint(f" - Plunges: %.1f {UNIT_LONG} (%.1f sec)" % (plungelen, plungelen / (glob_args.plunge_rate / 60))) 255 | eprint(f" - Moves: %.1f {UNIT_LONG} (%.1f sec)" % (movelen, movelen / (glob_args.feed_rate / 60))) 256 | eprint(f"Feed rate: %.1f {UNIT_LONG}/min" % (glob_args.feed_rate)) 257 | eprint(f"Plunge rate: %.1f {UNIT_LONG}/min" % (glob_args.plunge_rate)) 258 | eprint(f"Estimated machining time: %.1f sec" % (pathtime)) 259 | 260 | if hasattr(glob_args, 'json_dest'): 261 | with open(glob_args.json_dest, 'w') as f: 262 | # only dump stats relevant to web frontend 263 | stats = { 264 | 'output_dimensions': { 265 | 'width': img_size[0], 266 | 'height': img_size[1] 267 | }, 268 | 'line_width': line_width, 269 | 'line_spacing': line_spacing, 270 | 'nlines': int(nlines), 271 | 'img_ppi': img_ppi, 272 | 'output_ppi': output_ppi, 273 | 'interp_ppi': interp_ppi, 274 | 'pathlen': pathlen, 275 | 'pathtime': pathtime 276 | } 277 | 278 | json.dump(stats, f) 279 | 280 | def getPreambleEpilogue(): 281 | global glob_args 282 | pre, epi = DEF_PREAMBLE, DEF_EPILOGUE 283 | if hasattr(glob_args, 'preamble'): 284 | pre = glob_args.preamble 285 | elif hasattr(glob_args, 'preamble_file'): 286 | with open(glob_args.preamble_file) as f: 287 | pre = f.read() 288 | 289 | if hasattr(glob_args, 'epilogue'): 290 | epi = glob_args.epilogue 291 | elif hasattr(glob_args, 'epilogue_file'): 292 | with open(glob_args.epilogue_file) as f: 293 | epi = f.read() 294 | 295 | return pre, epi 296 | 297 | def setMetric(): 298 | global DEF_PREAMBLE, UNIT_LONG, UNIT_SHORT 299 | DEF_PREAMBLE = DEF_PREAMBLE_METRIC 300 | UNIT_LONG, UNIT_SHORT = MM_LONG, MM_SHORT 301 | 302 | def doEngrave(): 303 | # check parameter sanity 304 | for c in CONSTRAINTS: 305 | if not checkCondition(c): 306 | eprint("Refusing to generate G-code.") 307 | return 308 | 309 | if hasattr(glob_args, 'metric'): 310 | setMetric() 311 | 312 | preamble, epilogue = getPreambleEpilogue() 313 | 314 | # invert and convert to grayscale 315 | img = ~cv2.cvtColor(cv2.imread(glob_args.filename), cv2.COLOR_BGR2GRAY) 316 | 317 | orig_h, orig_w = orig_size = img.shape[:2] # pixels 318 | 319 | img_w, img_h = img_size = (glob_args.width, glob_args.width * (orig_h / orig_w)) if hasattr(glob_args, 'width') else (glob_args.height * (orig_w / orig_h), glob_args.height) # inches 320 | 321 | img_ppi = orig_w / img_w # should be the same for X and Y directions 322 | 323 | depth2width = 2 * math.tan(glob_args.tool_angle / 2 * DEG2RAD) # multiply by this to get the width of a cut 324 | line_width = glob_args.max_depth * depth2width 325 | line_spacing = glob_args.stepover * line_width / 100.0 # orthogonal distance between lines 326 | output_ppi = 1 / glob_args.linear_resolution # linear PPI of engraved image 327 | 328 | # scale up the image with interpolation 329 | # we want the image DPI to match our engraving DPI (which is glob_args.linear_resolution) 330 | scale_factor = SUPERSAMPLE * output_ppi / img_ppi 331 | img_interp = cv2.resize(img, None, fx = scale_factor, fy = scale_factor) 332 | interp_ppi = img_ppi * scale_factor 333 | 334 | print("( Generated by rastercarve: github.com/built1n/rastercarve )") 335 | print("( Image name: %s )" % (glob_args.filename)) 336 | 337 | gcode_multiline(preamble) 338 | 339 | d = np.array([math.cos(glob_args.line_angle * DEG2RAD), 340 | -math.sin(glob_args.line_angle * DEG2RAD)]) 341 | 342 | max_y = img_h + img_w * -d[1] / d[0] # highest Y we'll loop to 343 | yspace = line_spacing / math.cos(glob_args.line_angle * DEG2RAD) # vertical spacing between lines 344 | xspace = line_spacing / math.sin(glob_args.line_angle * DEG2RAD) if glob_args.line_angle != 0 else 0 # horizontal space 345 | 346 | nlines = round(max_y / yspace) 347 | 348 | ### Generate toolpath 349 | moveRapid(0, 0, glob_args.safe_z) 350 | end = None 351 | 352 | for y in tqdm(frange(0, max_y - yspace, yspace * 2), 353 | total = nlines / 2, 354 | desc = 'Generating G-code', 355 | unit = ' lines', 356 | unit_scale = 2, 357 | disable = hasattr(glob_args, 'quiet')): # we engrave two lines per loop 358 | start = np.array([0, y]).astype('float64') 359 | 360 | # start some vectors on the bottom edge of the image 361 | if d[1] != 0: 362 | c = (img_h - y) / d[1] # solve (start + cd)_y = h for c 363 | if c >= 0: 364 | start += c * d 365 | 366 | start = engraveLine(img_interp, img_size, interp_ppi, start, d, glob_args.linear_resolution) 367 | 368 | # now engrave the other direction 369 | # we just need to flip d and move start over 370 | 371 | # see which side of the image the last line ran out on (either top or right side) 372 | last = start + glob_args.linear_resolution * d 373 | 374 | if last[1] < 0: 375 | start[0] += xspace 376 | else: 377 | start[1] += yspace 378 | 379 | # check if we ran out the top-right corner (this needs special treatment) 380 | if start[0] >= img_w: 381 | debug("Special case TRIGGERED") 382 | c = (start[0] - img_w) / d[0] 383 | start -= c * d 384 | 385 | end = engraveLine(img_interp, img_size, interp_ppi, start, -d, glob_args.linear_resolution) 386 | 387 | moveSlow(end[0], end[1], glob_args.traverse_z) 388 | moveRapid(0, 0, glob_args.traverse_z) 389 | 390 | gcode_multiline(epilogue) 391 | 392 | dump_stats(orig_size, img_size, line_width, line_spacing, 393 | nlines, img_ppi, output_ppi, scale_factor, interp_ppi, pathlen, 394 | rapidlen, plungelen, movelen, pathtime) 395 | 396 | if not hasattr(glob_args, 'debug') and debug_msgs > 0: 397 | eprint("%d suppressed debug message(s)." % (debug_msgs)) 398 | 399 | def main(): 400 | parser = argparse.ArgumentParser(prog='rastercarve', 401 | formatter_class=argparse.ArgumentDefaultsHelpFormatter, 402 | description='Generate G-code to engrave raster images.', 403 | epilog= 404 | """ 405 | The default feeds have been found to be safe values for medium-density 406 | fiberboard (MDF). Experimenting with the STEPOVER, LINE_ANGLE, and 407 | LINEAR_RESOLUTION may yield improvements in engraving quality at the 408 | cost of increased machining time. 409 | 410 | On ShopBot machines, the --no-line-numbers flag must not be used, 411 | since the spindle will fail to start and damage the material. Use this 412 | flag with caution on other machines.""") 413 | parser.add_argument('filename', help='input image (any OpenCV-supported format)') 414 | 415 | dim_group = parser.add_argument_group('output dimensions', 'Exactly one required. Image will be scaled while maintaining aspect ratio.') 416 | mutex_group = dim_group.add_mutually_exclusive_group(required=True) 417 | mutex_group.add_argument('--width', help='output width (in)', action='store', dest='width', type=float, default=argparse.SUPPRESS) 418 | mutex_group.add_argument('--height', help='output height (in)', action='store', dest='height', type=float, default=argparse.SUPPRESS) 419 | 420 | mach_group = parser.add_argument_group('machine configuration') 421 | mach_group.add_argument('-f', help='engraving feed rate (in/min)', action='store', dest='feed_rate', default=DEF_FEED_RATE, type=float) 422 | mach_group.add_argument('-p', help='engraving plunge rate (in/min)', action='store', dest='plunge_rate', default=DEF_PLUNGE_RATE, type=float) 423 | mach_group.add_argument('--rapid', help='rapid traverse rate (for time estimation only)', action='store', dest='rapid_rate', default=DEF_RAPID_RATE, type=float) 424 | mach_group.add_argument('-z', help='rapid traverse height (in)', action='store', dest='safe_z', default=DEF_SAFE_Z, type=float) 425 | mach_group.add_argument('--end-z', help='Z height of final traverse (in)', action='store', dest='traverse_z', default=DEF_TRAVERSE_Z, type=float) 426 | mach_group.add_argument('-t', help='included angle of tool (deg)', action='store', dest='tool_angle', default=DEF_TOOL_ANGLE, type=float) 427 | 428 | cut_group = parser.add_argument_group('engraving parameters') 429 | cut_group.add_argument('-d', help='maximum engraving depth (in)', action='store', dest='max_depth', default=DEF_MAX_DEPTH, type=float) 430 | cut_group.add_argument('-a', help='angle of grooves from horizontal (deg)', action='store', dest='line_angle', default=DEF_LINE_ANGLE, type=float) 431 | cut_group.add_argument('-s', help='stepover percentage (affects spacing between lines)', action='store', dest='stepover', default=DEF_STEPOVER, type=float) 432 | cut_group.add_argument('-r', help='distance between successive G-code points (in)', action='store', dest='linear_resolution', default=DEF_LINEAR_RESOLUTION, type=float) 433 | cut_group.add_argument('--dots', help='engrave using dots instead of lines (experimental)', action='store_true', dest='pointmode', default=argparse.SUPPRESS) 434 | 435 | 436 | gcode_group = parser.add_argument_group('G-code parameters') 437 | 438 | gcode_group.add_argument('--metric', help='change all units to millimeters (sets G21); cannot be used with --preamble or --preamble-file', action='store_true', dest='metric', default=argparse.SUPPRESS) 439 | gcode_group.add_argument('--no-line-numbers', help='suppress G-code line numbers (dangerous on ShopBot!)', action='store_true', dest='suppress_linenos', default=argparse.SUPPRESS) 440 | 441 | preamble_group = gcode_group.add_mutually_exclusive_group(required=False) 442 | preamble_group.add_argument('--preamble', help='override the default G-code preamble; to specify multiple lines on the command line, use $\'\' strings with \\n; each line of the preamble will be prepended with a line number, except when used with --no-line-numbers', action='store', dest='preamble', default=argparse.SUPPRESS) 443 | preamble_group.add_argument('--preamble-file', help='like --preamble, but read from a file', action='store', dest='preamble_file', default=argparse.SUPPRESS) 444 | 445 | epilogue_group = gcode_group.add_mutually_exclusive_group(required=False) 446 | epilogue_group.add_argument('--epilogue', help='override the default G-code epilogue; see above notes for --preamble', action='store', dest='epilogue', default=argparse.SUPPRESS) 447 | epilogue_group.add_argument('--epilogue-file', help='like --epilogue, but read from a file', action='store', dest='epilogue_file', default=argparse.SUPPRESS) 448 | 449 | parser.add_argument('--json', help='dump statistics in JSON format', action='store', dest='json_dest', default=argparse.SUPPRESS) 450 | parser.add_argument('--debug', help='print debug messages', action='store_true', dest='debug', default=argparse.SUPPRESS) 451 | parser.add_argument('-q', help='disable progress and statistics', action='store_true', dest='quiet', default=argparse.SUPPRESS) 452 | parser.add_argument('--version', help="show program's version number and exit", action='version', version=__version__) 453 | 454 | global glob_args 455 | glob_args = parser.parse_args() 456 | debug(glob_args) 457 | 458 | doEngrave() 459 | 460 | if __name__=="__main__": 461 | main() 462 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import setuptools 4 | from rastercarve import __version__ 5 | 6 | with open("README.md", "r") as fh: 7 | long_description = fh.read() 8 | 9 | setuptools.setup( 10 | name="rastercarve", # Replace with your own username 11 | version=__version__, 12 | author="Franklin Wei", 13 | author_email="franklin@rockbox.org", 14 | description="Generate G-code to engrave raster images", 15 | long_description=long_description, 16 | long_description_content_type="text/markdown", 17 | url="https://github.com/built1n/rastercarve", 18 | packages=setuptools.find_packages(), 19 | classifiers=[ 20 | "Development Status :: 5 - Production/Stable", 21 | "Environment :: Console", 22 | "License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)", 23 | "Operating System :: OS Independent", 24 | "Programming Language :: Python :: 3", 25 | "Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)", 26 | "Topic :: Utilities", 27 | ], 28 | python_requires='>=3.6', 29 | install_requires=["numpy", "tqdm", "argparse"] + ["opencv-python"] if os.getenv("SETUP_IGNORE_OPENCV", "0")=="0" else [], 30 | entry_points={ 31 | "console_scripts": [ 32 | "rastercarve=rastercarve.__main__:main", 33 | ] 34 | } 35 | ) 36 | --------------------------------------------------------------------------------