junglejourney
view materials/make_packaging.py @ 184:cd78196f8910
Resized the page to match the size of a cassette inlay I measured.
| author | David Boddie <david@boddie.org.uk> |
|---|---|
| date | Mon Sep 26 00:18:54 2011 +0200 |
| parents | c3cc532640a5 |
| children | ae1d814100fa |
line source
1 #!/usr/bin/env python
3 """
4 Copyright (C) 2011 David Boddie <david@boddie.org.uk>
6 This program is free software: you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation, either version 3 of the License, or
9 (at your option) any later version.
11 This program is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
16 You should have received a copy of the GNU General Public License
17 along with this program. If not, see <http://www.gnu.org/licenses/>.
18 """
20 import codecs, os, sys
21 from PyQt4.QtCore import QSize
22 from PyQt4.QtGui import *
24 def relpath(source, destination):
26 source = os.path.abspath(source)
27 destination = os.path.abspath(destination)
29 src_pieces = source.split(os.sep)
30 dest_pieces = destination.split(os.sep)
32 if os.path.isfile(source):
33 src_pieces.pop()
35 common = []
36 for i in range(min(len(src_pieces), len(dest_pieces))):
38 if src_pieces[i] == dest_pieces[i]:
39 common.append(src_pieces[i])
40 i -= 1
41 else:
42 break
44 to_common = os.sep.join([os.pardir]*(len(src_pieces)-len(common)))
45 return to_common + os.sep + os.sep.join(dest_pieces[len(common):])
48 class SVG:
50 def __init__(self, path):
52 self.path = path
54 def _escape(self, text):
56 for s, r in (("&", "&"), ("<", "<"), (">", ">")):
57 text = text.replace(s, r)
59 return text
61 def open(self):
63 self.text = ('<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n'
64 '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"\n'
65 ' "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n'
66 '<svg version="1.1"\n'
67 ' xmlns="http://www.w3.org/2000/svg"\n'
68 ' xmlns:xlink="http://www.w3.org/1999/xlink"\n'
69 ' width="6.5cm" height="10.0cm"\n'
70 ' viewBox="0 0 650 1000">\n')
72 def add_image(self, x, y, width, height, path):
74 path = os.path.join(relpath(self.path, os.curdir), path)
75 self.text += '<image x="%f" y="%f" width="%f" height="%f"\n' % (x, y, width, height)
76 self.text += ' xlink:href="%s" />\n' % path
78 def add_text(self, x, y, font, text):
80 self.text += '<text x="%f" y="%f"\n' % (x, y)
81 self.text += (' font-family="%s"\n'
82 ' font-size="%i"\n') % (font["family"], font["size"])
83 if font.has_key("weight"):
84 self.text += ' font-weight="%s"\n' % font["weight"]
85 if font.has_key("style"):
86 self.text += ' font-style="%s"\n' % font["style"]
87 self.text += '>\n'
88 self.text += self._escape(text)
89 self.text += '</text>\n'
91 def close(self):
93 self.text += "</svg>\n"
94 codecs.open(self.path, "w", "utf-8").write(self.text)
97 class Page:
99 def __init__(self, size, objects):
101 self.size = size
102 self.objects = objects
104 def render(self, svg):
106 positions = [(0, 0)]
107 for obj in self.objects:
109 x, y = obj.render(svg, positions)
110 positions.append((x, y))
112 return svg
114 class TextBox:
116 def __init__(self, bbox, text_items, follow = False, index = -1):
118 self.bbox = bbox
119 self.text_items = text_items
120 self.follow = follow
121 self.index = index
123 def render(self, svg, positions):
125 x, y, width, height = self.bbox
127 if self.follow:
128 y = y + positions[self.index][1]
130 for text_item in self.text_items:
132 left_indent = text_item.font.get("left indent", 0)
133 right_indent = text_item.font.get("right indent", 0)
134 item_x = x + left_indent
135 item_width = width - left_indent - right_indent
137 for pieces, line_height in text_item.readline(item_width):
139 for font, word_x, text in pieces:
141 svg.add_text(item_x + word_x, y, font, text)
143 y += line_height
145 return x, y
147 class Text:
149 def __init__(self, font, text):
151 self.font = font
152 self.text = text
154 self.parse_text()
156 def parse_text(self):
158 lines = self.text.split("\n")
159 self.lines = []
161 for line in lines:
163 words = []
164 for word in line.split():
166 words.append(Word(self.font, word))
168 self.lines.append(words)
170 def readline(self, width):
172 for line in self.lines:
174 w = 0
175 used = 0
176 words = []
178 while w < len(line):
180 word = line[w]
181 word_width = word.width()
183 if used + word_width <= width:
184 # Add words while there is still space.
185 used += word_width + word.space()
186 words.append(word)
187 w += 1
189 elif words:
190 # When out of space, yield the words on the line.
191 yield self.format(words, width), self.height(words)
193 used = 0
194 words = []
196 else:
197 # If no words will fit on the line, just add the first
198 # word to the list.
199 yield self.format([word], width), self.height(words)
201 used = 0
202 w += 1
204 if words:
205 yield self.format(words, width, last = True), self.height(words)
206 elif not line:
207 yield [], self.line_height()/2
209 def format(self, words, width, last = False):
211 output = []
212 x = 0
214 if len(words) == 0:
215 spacing = 0
217 elif self.font.get("align", "left") == "justify" and not last:
218 # Full justify the text.
219 total_width = sum(map(lambda word: word.width(), words))
220 spacing = (width - total_width)/float(len(words) - 1)
222 elif self.font.get("align", "left") == "centre":
223 # Centre the text.
224 total_width = sum(map(lambda word: word.width(), words))
225 total_space = sum(map(lambda word: word.space(), words)[:-1])
226 x = width/2.0 - total_width/2.0 - total_space/2.0
227 spacing = None
229 else:
230 spacing = None
232 for word in words:
234 output.append((word._font, x, word.text))
235 x += word.width()
236 if spacing is not None:
237 x += spacing
238 else:
239 x += word.space()
241 return output
243 def height(self, words):
245 return max(map(lambda word: word.height(), words))
247 def line_height(self):
249 font = QFont(self.font.get("family"))
250 font.setPixelSize(self.font.get("size"))
251 if self.font.get("weight") == "bold":
252 font.setWeight(QFont.Bold)
253 if self.font.get("style") == "italic":
254 font.setItalic(True)
256 metrics = QFontMetrics(font)
257 return metrics.height()
259 class Word:
261 def __init__(self, font, text):
263 self._font = font
264 self.text = text
266 def font(self):
268 font = QFont(self._font.get("family"))
269 font.setPixelSize(self._font.get("size"))
270 if self._font.get("weight") == "bold":
271 font.setWeight(QFont.Bold)
272 if self._font.get("style") == "italic":
273 font.setItalic(True)
274 return font
276 def width(self):
278 metrics = QFontMetrics(self.font())
279 return metrics.width(self.text)
281 def height(self):
283 metrics = QFontMetrics(self.font())
284 return metrics.height()
286 def space(self):
288 metrics = QFontMetrics(self.font())
289 return metrics.width(" ")
292 class Image:
294 def __init__(self, bbox, path, scale = None, follow = False, index = -1):
296 self.bbox = bbox
297 self.path = path
298 self.follow = follow
299 self.index = index
300 self.scale = scale
302 def render(self, svg, positions):
304 x, y, width, height = self.bbox
306 if self.follow:
307 y = y + positions[self.index][1]
309 im = QImage(self.path)
310 width = im.size().width()
311 height = im.size().height()
313 if self.scale:
314 width = width * self.scale
315 height = height * self.scale
317 svg.add_image(x, y, width, height, self.path)
319 return x + width, y + height
322 if __name__ == "__main__":
324 app = QApplication(sys.argv)
326 if len(app.arguments()) != 2:
328 sys.stderr.write("Usage: %s <output directory>\n" % app.arguments()[0])
329 sys.exit(1)
331 output_dir = sys.argv[1]
333 if not os.path.exists(output_dir):
334 os.mkdir(output_dir)
336 regular = {"family": "FreeSerif",
337 "size": 24,
338 "align": "justify"}
340 title = {"family": "FreeSerif",
341 "size": 24,
342 "weight": "bold"}
344 italic_quote = {"family": "FreeSerif",
345 "size": 22,
346 "style": "italic",
347 "left indent": 40,
348 "right indent": 40}
350 quote = {"family": "FreeSerif",
351 "size": 22,
352 "left indent": 40,
353 "right indent": 40}
355 monospace_quote = {"family": "FreeMono",
356 "size": 24,
357 "left indent": 40,
358 "right indent": 40}
360 keys_quote = {"family": "FreeSerif",
361 "size": 24,
362 "left indent": 40,
363 "right indent": 40}
365 key_descriptions_quote = {"family": "FreeSerif",
366 "size": 24,
367 "left indent": 160,
368 "right indent": 40}
370 exclamation = {"family": "FreeSerif",
371 "size": 28,
372 "style": "italic",
373 "weight": "bold",
374 "align": "centre"}
376 back_cover_title = {"family": "FreeSerif",
377 "size": 36,
378 "weight": "bold",
379 "align": "centre"}
381 back_cover_subtitle = {"family": "FreeSerif",
382 "size": 28,
383 "weight": "bold",
384 "align": "centre"}
386 back_cover_centred = {"family": "FreeSerif",
387 "size": 24,
388 "align": "centre"}
390 pages = [
391 Page((650, 1020),
392 [TextBox((25, 35, 600, 0),
393 [Text(title, "Jungle Journey\n"),
394 Text(regular,
395 "The last flames of the campfire fade to glowing embers and I am alone. "
396 "My recent acquaintances, their packs and paraphernalia have gone, leaving "
397 "me stranded deep in the heart of this jungle realm. Clouds momentarily "
398 "sweep the cold face of the moon and I perceive the clicks, whistles and "
399 "cries of creatures in the hot air that cloaks this place. Desperately, I "
400 "try to stay my panic and remember those fragments of wilderness craft "
401 "learned and unlearned many years ago.\n"),
402 Text(italic_quote,
403 "Choose your weapon carefully,\n"
404 "Get ready for a fight.\n"
405 "The jungle can be dangerous\n"
406 "If you go there at night.\n"
407 "There's time to pick up treasure,\n"
408 "But no time to stop and stare.\n"
409 "If you don't find the hidden cave\n"
410 "You won't get out of there.\n"),
411 Text(regular,
412 "Hopeless, I scramble to my feet, reaching for any weapon still left to me. "
413 "Struggling through the dense undergrowth, I search for signs of a track or "
414 "trail. At first glance, paths that seemed to lead to safety turn out to be "
415 "impassable, overgrown by tangled and twisted vines. I remember the words of "
416 "an old teacher:\n"),
417 Text(quote,
418 u'\u201cDo not be tempted to use fire to make your way. '
419 'Many a traveller has strayed from the path, using fire to blaze a trail, '
420 'only to reach a dead end. Trying to return, they find that the jungle '
421 'has grown back. Those who are desperate enough will even seek out '
422 u'forgotten routes when the way home is in sight.\u201d\n'),
423 Text(regular,
424 "Sensing my presence, obscene creatures emerge from the darkness, hungry "
425 "for prey. Only through skill and luck am I able to dispatch them back "
426 "into the shadows. Even though I know I must journey deeper into this "
427 "uncharted land to find the way home, the thought of vengeance drives me on.")
428 ])
429 ]),
430 Page((650, 1000),
431 [TextBox((25, 35, 600, 0),
432 [Text(title, "Loading the Game\n"),
433 Text(regular, "Insert the cassette or disk and type\n"),
434 Text(monospace_quote, "*RUN JUNGLE\n"),
435 Text(regular,
436 "then press Return. If you are loading the game from cassette, press play on the "
437 "cassette recorder. The game should now load.\n"),
438 Text(title, "Playing the Game\n"),
439 Text(regular,
440 "The player must help the character reach the exit for each level. However, the "
441 "player must first find a key to unlock the exit. On the final level, the exit "
442 "does not require a key but it may be obstructed. Enemies will appear in each "
443 "location and attack the player's character. These can be destroyed by "
444 "projectiles fired by the current weapon.\n"),
445 Text(regular,
446 "Your character can be moved around the screen by using four control keys:\n")]),
447 TextBox((25, 0, 600, 0),
448 [Text(keys_quote,
449 "Z\n"
450 "X\n"
451 ":\n"
452 "/")], follow = True),
453 TextBox((25, 0, 600, 0),
454 [Text(key_descriptions_quote,
455 "left\n"
456 "right\n"
457 "up\n"
458 "down\n"),
459 Text(regular,
460 "To fire a weapon, press the Return key. There are four different types of "
461 "weapon available in the game.\n\n"
462 "Alternatively, you may may using an analogue joystick connected to a Plus 1 "
463 "expansion interface. Select joystick controls by pressing the J key on the "
464 "title page. Press K to select keyboard controls.\n\n"
465 "Other keys can be used to control the game:\n")],
466 follow = True, index = -2),
467 TextBox((25, 0, 600, 0),
468 [Text(keys_quote,
469 "S\n"
470 "Q\n"
471 "P\n"
472 "O\n"
473 "Escape")], follow = True),
474 TextBox((25, 0, 600, 0),
475 [Text(key_descriptions_quote,
476 "enable sound effects\n"
477 "disable sound effects\n"
478 "pause the game\n"
479 "resume the game\n"
480 "quit the game, returning to the title screen\n")],
481 follow = True, index = -2)
482 ]),
483 Page((650, 1000),
484 [TextBox((25, 35, 600, 0),
485 [Text(title, "Treasure\n"),
486 Text(regular, "Items of treasure can be found throughout the jungle. "
487 "Pick these up to increase your score.\n")]),
488 Image((45, -8, 515, 0), "../images/key.xpm", scale = 4,
489 follow = True),
490 TextBox((135, 20, 475, 0),
491 [Text(regular, "Find the key to open the door on all levels except the last. "
492 "Each key is worth 50 points.")],
493 follow = True, index = -2),
494 Image((45, 8, 515, 0), "../images/chest.xpm", scale = 4,
495 follow = True, index = -2),
496 TextBox((135, 48, 475, 0),
497 [Text(regular, "Treasure chests are worth 20 points.")],
498 follow = True, index = -3),
499 Image((45, 8, 515, 0), "../images/jewel.xpm", scale = 4,
500 follow = True, index = -2),
501 TextBox((135, 48, 475, 0),
502 [Text(regular, "Jewels are worth 5 points.")],
503 follow = True, index = -3),
504 Image((45, 8, 515, 0), "../images/statue.xpm", scale = 4,
505 follow = True, index = -2),
506 TextBox((135, 48, 475, 0),
507 [Text(regular, "Statues are worth 10 points.")],
508 follow = True, index = -3),
509 Image((45, 8, 515, 0), "../images/health.xpm", scale = 4,
510 follow = True, index = -2),
511 TextBox((135, 36, 475, 0),
512 [Text(regular, "Presents are worth 40 points and boost your strength by 20 units.")],
513 follow = True, index = -3),
514 TextBox((25, 48, 600, 0),
515 [Text(title, "Exits\n"),
516 Text(regular, "Each level has an exit that can be opened using a key. "
517 "On the last level you will find a gate that leads to safety. "
518 "This does not require a key, but it is well hidden.\n")],
519 follow = True),
520 Image((77, -4, 513, 0), "../images/exit1.xpm", scale = 4,
521 follow = True),
522 TextBox((215, 36, 400, 0),
523 [Text(regular, "The exit is initially locked. Find the key to unlock it.")],
524 follow = True, index = -2),
525 Image((45, 8, 545, 0), "../images/finalexitl.xpm", scale = 4,
526 follow = True, index = -2),
527 Image((109, 8, 481, 0), "../images/finalexitr.xpm", scale = 4,
528 follow = True, index = -3),
529 TextBox((215, 48, 400, 0),
530 [Text(regular, "The final exit is hidden somewhere on the final level.")],
531 follow = True, index = -4),
532 TextBox((25, 950, 600, 0),
533 [Text(exclamation, "Have a safe journey!")])
534 ]),
535 Page((650, 1000),
536 [TextBox((25, 50, 600, 0),
537 [Text(back_cover_title, "Jungle Journey"),
538 Text(back_cover_subtitle, "for the Acorn Electron and BBC Model B")]),
539 Image((100, 8, 500, 0), "screenshot1.png", scale = 0.4, follow = True),
540 TextBox((25, 900, 600, 0),
541 [Text(back_cover_centred,
542 u"Copyright \u00a9 2011 David Boddie\n"
543 u"An Infukor production for Retro Software\n"
544 u"http://www.retrosoftware.co.uk/")]),
545 ]),
546 ]
548 i = 0
549 for page in pages:
551 path = os.path.join(output_dir, "page-%i.svg" % i)
552 svg = SVG(path)
553 svg.open()
554 page.render(svg)
555 svg.close()
556 i += 1
558 sys.exit()
